跳转到内容

自定义

这里按改动范围列出常见需求:改样式、替换按钮或大图、增加自己的控件, 以及翻译文案。每一项都先给出改动最小的做法,大多数情况下不需要退到完全组合。

想做什么 用什么 需要重写界面吗
换颜色、圆角、间距 CSS 自定义属性 不需要
深度重写某个部件的样式 data-* 选择器 不需要
换掉某个按钮的元素或图标 asChild / children 不需要
增加、删除、重排按钮 自己组合 Content 需要,但用的是同一批部件
更换大图组件或监听加载事件 renderImage 不需要
加快捷键 Extensions 不需要
翻译界面文案 labels 不需要

最快的改法。预设样式表里所有取值都来自自定义属性, 覆盖它们就能重新配色,一个选择器都不用写。

:root {
--riv-accent: #0f766e; /* 焦点环、控件激活态 */
--riv-accent-surface: #ccfbf1; /* 激活控件的背景 */
--riv-chrome: #ffffff; /* 顶栏和工具栏的表面色 */
--riv-stage: #f4f4f5; /* 图片后面的区域 */
--riv-ink: #18181b; /* 主文字与图标 */
--riv-ink-muted: #71717a; /* 次级文字 */
--riv-line: #e4e4e7; /* 边框与分隔线 */
--riv-hover: #f4f4f5; /* 控件 hover 背景 */
--riv-radius: 12px; /* 弹窗圆角 */
--riv-radius-toolbar: 10px; /* 工具栏圆角 */
}

设置在 :root 或 dialog 元素上,不要设置在某个祖先类上。预览器会 portal 到 document.body,作用域限定在包裹类上的规则根本够不着它。 这也是深色模式绑定在 prefers-color-scheme 加显式的 data-riv-theme="light" | "dark"(写在 :root 或 dialog 上)、 而不是绑定在某个容器的 .dark 类上的原因。

每个部件都通过 data-* 暴露自己的身份和状态, 你可以用原生 CSS 或 Tailwind 变体直接命中,不需要知道任何类名。

/* 某一个具体控件 */
[data-image-view-control='actual-size'] {
font-variant-numeric: tabular-nums;
}
/* 是状态,不是类名 */
[data-image-view-control][data-active] {
outline: 2px solid var(--riv-accent);
}
[data-image-view-control][data-boundary] {
opacity: 0.35;
}
/* 区域 */
[data-image-view-region='toolbar'] {
backdrop-filter: blur(8px);
}
[data-image-view-stage][data-phase='dismissing'] {
cursor: grabbing;
}
// 同样的钩子在 Tailwind 里
<ImageView.ActualSize className="data-[active]:bg-teal-100 data-[boundary]:opacity-40" />

完整清单:data-image-view(dialog)、-stage-viewport-track-slide-trigger-title-counter-loading-error-thumbnails-thumb-region="header|toolbar|footer",以及 -control="close|prev|next|zoom-in|zoom-out|rotate-left|rotate-right|fit|actual-size|download|retry"。 状态:data-activedata-boundarydata-disableddata-currentdata-phasedata-statedata-closing

要换掉控件的元素本身 —— 比如换成你设计系统里的按钮,或者外面包一层 tooltip —— 传 asChild 并把元素交给它。控件会把行为、aria-labeldata-* 合并到你的子元素上,不再渲染自己的 <button>

import { Button } from '@/components/ui/button'
function ZoomButton() {
return (
<ImageView.ZoomIn asChild>
<Button variant="ghost" size="icon">
<PlusIcon />
</Button>
</ImageView.ZoomIn>
)
}

只想换图标或文字的话,直接传 children,不需要 asChild

<ImageView.Close>
<XIcon /> 收起
</ImageView.Close>

要增加、删除或重排按钮,就自己写外壳。用的是 DefaultContent 所用的同一批部件 —— 它底下没有任何私有实现 —— 所以这么做不会损失任何能力。

主入口的规则是:如果 Group 在直接子节点里找到了 <ImageView.Content>(或 <DefaultContent>),它就什么都不再补。 不要把这一层藏进包装组件。 想让自定义界面尽可能轻,就用下面的 headless 入口:它的 Group 从不引入默认预设。

import { ImageView } from 'react-img-view/primitives'
function CustomViewer({ images }) {
return (
<ImageView.Group images={images}>
{images.map((image, i) => (
<ImageView key={image.src} index={i} {...image}>
<img src={image.src} alt={image.name} />
</ImageView>
))}
<ImageView.Content className="riv-dialog">
<ImageView.Header className="riv-header">
<ImageView.Close>关闭</ImageView.Close>
<ImageView.Title />
<ImageView.Counter />
<span className="flex-1" />
{/* 你自己的按钮,就放在内置控件旁边 */}
<button onClick={() => window.print()}>打印</button>
<ImageView.Download>下载</ImageView.Download>
</ImageView.Header>
<ImageView.Stage className="riv-stage">
<ImageView.Image />
<ImageView.Prev></ImageView.Prev>
<ImageView.Next></ImageView.Next>
<ImageView.Loading>载入中…</ImageView.Loading>
<ImageView.Error>
{({ retry }) => (
<div>
<p>无法加载这张图片</p>
<button onClick={retry}>重试</button>
</div>
)}
</ImageView.Error>
{/* 去掉了旋转,保留缩放,顺序也改了 —— 没有哪个是必须的 */}
<ImageView.Toolbar className="riv-toolbar">
<ImageView.ZoomOut />
<ImageView.ZoomIn />
<ImageView.FitToWindow />
<ImageView.ActualSize />
</ImageView.Toolbar>
</ImageView.Stage>
</ImageView.Content>
</ImageView.Group>
)
}
Woman holding a vintage camera
camera-portrait.webp
Steam rising across a green geothermal landscape
geothermal-landscape.webp
Woman writing in a journal in a sunlit forest
forest-journal.webp
Star-filled night sky above a forest
starry-night.webp

useViewer()Group 内部任意位置都能用,所以自定义控件就是一个调用 API 的按钮。

import { useViewer } from 'react-img-view'
function ZoomReadout() {
const viewer = useViewer()
return <span>{Math.round(viewer.scale * 100)}%</span>
}
function PrintButton() {
const viewer = useViewer()
return (
<button onClick={() => window.print()} disabled={viewer.status !== 'ready'}>
打印
</button>
)
}

要从 Group 外部打开 —— 比如表格行里的「查看」按钮 —— 直接调用函数式 API:

import { ImagePreview } from 'react-img-view/imperative'
function ViewButton({ images }) {
return <Button onClick={() => ImagePreview.open({ images, index: 2 })}>查看第 3 张</Button>
}

如果外围组件本来就需要读取或持久化打开状态和当前序号,再使用受控模式:

const [open, setOpen] = useState(false);
const [index, setIndex] = useState(0);
<ImageView.Group
images={images}
open={open}
index={index}
onOpenChange={setOpen}
onIndexChange={setIndex}
/>
<Button onClick={() => { setIndex(2); setOpen(true); }}>查看第 3 张</Button>

需要给大图增加 referrerPolicysrcSet 等属性,换成自己的图片组件,或者记录 加载成功与失败事件时,使用 renderImage。库会把维持尺寸、加载状态、重试和变换 所需的 imageProps 交给你;最终渲染出的 <img> 需要把它们透传进去。

const renderImage = ({ item, imageProps }) => (
<picture>
<source srcSet={`${item.src}.webp`} type="image/webp" />
<img
{...imageProps}
referrerPolicy="no-referrer"
onLoad={(event) => {
imageProps.onLoad?.(event)
analytics.track('image_loaded', { src: item.src })
}}
/>
</picture>
)
<ImageView.Group images={images}>
{/* triggers */}
<ImageView.DefaultContent renderImage={renderImage} />
</ImageView.Group>

如果要覆盖 onLoadonError,应像示例一样先调用 imageProps 中原有的处理函数; 直接丢掉它们会让加载状态和重试失效。同一个 renderImage 也可以传给单图入口 <ImageView>ImageView.ImageImagePreview.open()

组合方式覆盖不到的键盘行为,用 extension 补充。返回 true 表示事件已被消费, 内置处理会被跳过。

const pageWithSpace = {
name: 'space-pages',
onKeyDown(event, api) {
if (event.key !== ' ') return
if (event.shiftKey) api.prev()
else api.next()
return true // 已处理,不要继续往下走
},
}
function Viewer({ images }) {
return <ImageView.Group images={images} extensions={[pageWithSpace]} />
}

指针手势继续由 Stage 中经过测试的状态机统一管理。任何需要渲染的东西都应该 作为子节点放进组件树;extension 只是键盘行为的补充入口,不是一套插件系统。

所有面向用户的文案都来自同一份配置。其中大多数只以 aria-label 的形式出现 ——errorTitle 是默认界面里唯一还会显示成文字的一项。不传 labels 时, Group 使用稳定的英文默认值。显式传入 labels 即可覆盖;它按字段合并, 只覆盖传入的字段,其余不受影响。

<ImageView.Group images={images} labels={{ close: '关闭', download: '下载', zoomIn: '放大' }} />

简体中文建议直接按需引入完整语言包:

import zhCN from 'react-img-view/locales/zh-CN'
function ChineseViewer({ images }) {
return <ImageView.Group images={images} labels={zhCN} />
}
Woman holding a vintage camera
camera-portrait.webp
Steam rising across a green geothermal landscape
geothermal-landscape.webp

自定义控件应该用 useLabels() 读同一份文案,这样整个应用只需要翻译一处:

import { useLabels } from 'react-img-view'
function MyClose() {
const labels = useLabels()
return <ImageView.Close aria-label={labels.close}>{labels.close}</ImageView.Close>
}

完整的键名清单见 ViewerLabels