自定义
这里按改动范围列出常见需求:改样式、替换按钮或大图、增加自己的控件, 以及翻译文案。每一项都先给出改动最小的做法,大多数情况下不需要退到完全组合。
改动范围速查
Section titled “改动范围速查”| 想做什么 | 用什么 | 需要重写界面吗 |
|---|---|---|
| 换颜色、圆角、间距 | CSS 自定义属性 | 不需要 |
| 深度重写某个部件的样式 | data-* 选择器 |
不需要 |
| 换掉某个按钮的元素或图标 | asChild / children |
不需要 |
| 增加、删除、重排按钮 | 自己组合 Content |
需要,但用的是同一批部件 |
| 更换大图组件或监听加载事件 | renderImage |
不需要 |
| 加快捷键 | Extensions | 不需要 |
| 翻译界面文案 | labels |
不需要 |
1. CSS 自定义属性
Section titled “1. CSS 自定义属性”最快的改法。预设样式表里所有取值都来自自定义属性, 覆盖它们就能重新配色,一个选择器都不用写。
: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 类上的原因。
2. data-* 属性
Section titled “2. data-* 属性”每个部件都通过 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-active、data-boundary、data-disabled、data-current、
data-phase、data-state、data-closing。
3. asChild
Section titled “3. asChild”要换掉控件的元素本身 —— 比如换成你设计系统里的按钮,或者外面包一层 tooltip ——
传 asChild 并把元素交给它。控件会把行为、aria-label 和 data-*
合并到你的子元素上,不再渲染自己的 <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>4. 自己组合 Content
Section titled “4. 自己组合 Content”要增加、删除或重排按钮,就自己写外壳。用的是 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> )}



用你自己的界面驱动预览器
Section titled “用你自己的界面驱动预览器”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>5. renderImage
Section titled “5. renderImage”需要给大图增加 referrerPolicy、srcSet 等属性,换成自己的图片组件,或者记录
加载成功与失败事件时,使用 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>如果要覆盖 onLoad 或 onError,应像示例一样先调用 imageProps 中原有的处理函数;
直接丢掉它们会让加载状态和重试失效。同一个 renderImage 也可以传给单图入口
<ImageView>、ImageView.Image 或 ImagePreview.open()。
6. Extensions
Section titled “6. Extensions”组合方式覆盖不到的键盘行为,用 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 只是键盘行为的补充入口,不是一套插件系统。
7. Labels
Section titled “7. Labels”所有面向用户的文案都来自同一份配置。其中大多数只以 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} />}

自定义控件应该用 useLabels() 读同一份文案,这样整个应用只需要翻译一处:
import { useLabels } from 'react-img-view'
function MyClose() { const labels = useLabels() return <ImageView.Close aria-label={labels.close}>{labels.close}</ImageView.Close>}完整的键名清单见 ViewerLabels。