
ATRI
科幻import { Card, Image, Stack, Tag, Text } from '@hina-ui/react'
export default function Demo() {
return (
<Card className="w-full max-w-xs" padded={false}>
<Image
src="/sample.webp"
alt="夏日午后的坡道"
ratio={4 / 3}
lazy={false}
eager
className="rounded-t-lg"
/>
<Stack gap="xs" align="start" className="p-4">
<Text className="font-medium">ATRI</Text>
<Tag>科幻</Tag>
</Stack>
</Card>
)
}
用法
import { Image } from '@hina-ui/react'
图片在接近视口时才开始加载,加载期间由骨架占位,图片就绪后骨架淡出。用类名给出尺寸,或者用 ratio 提前占好高度。
import { Image } from '@hina-ui/react'
export default function Demo() {
return (
<Image src="/sample.webp" alt="夏日午后的坡道" className="h-40 w-full max-w-md rounded-md" />
)
}
示例
宽高比
ratio 是宽除以高。图片还不存在时框就已经是这个形状,因此图片到位不会顶动页面。
1 / 1
4 / 3
16 / 9
import { Image, Inline, Stack, Text } from '@hina-ui/react'
const ratios = [
{ label: '1 / 1', value: 1 },
{ label: '4 / 3', value: 4 / 3 },
{ label: '16 / 9', value: 16 / 9 },
]
export default function Demo() {
return (
<Inline align="start" className="gap-4">
{ratios.map(ratio => (
<Stack key={ratio.label} gap="xs" className="w-40">
<Text tone="muted" size="sm">
{ratio.label}
</Text>
<Image
src="/sample.webp"
alt="夏日午后的坡道"
ratio={ratio.value}
className="rounded-md"
/>
</Stack>
))}
</Inline>
)
}
外框与图片样式
className、style 设置外框,imageClass、imageStyle 设置内部的 img。style 中的 aspectRatio 会覆盖 ratio。
import { Image } from '@hina-ui/react'
export default function Demo() {
return (
<Image
src="/sample.webp"
alt="夏日午后的坡道"
preview
className="rounded-md"
style={{ width: '240px', height: '160px' }}
imageStyle={{ objectPosition: 'left center' }}
/>
)
}
填充方式
fit 决定图片如何填满外框,默认为 cover。
cover
contain
fill
import { Image, Inline, Stack, Text } from '@hina-ui/react'
const fits = ['cover', 'contain', 'fill'] as const
export default function Demo() {
return (
<Inline align="start" className="gap-4">
{fits.map(fit => (
<Stack key={fit} gap="xs" className="w-32">
<Text tone="muted" size="sm">
{fit}
</Text>
<Image
src="/sample.webp"
alt="夏日午后的坡道"
fit={fit}
ratio={1}
className="bg-inset rounded-md"
/>
</Stack>
))}
</Inline>
)
}
首屏图片
lazy 默认开启,图片要等观察器放行才开始请求,服务端渲染阶段也不会带上地址。首屏图片应当关闭 lazy 并开启 eager:地址随首屏 HTML 一同送达,请求以 fetchpriority="high" 发出,解码方式也改为同步。

import { Image } from '@hina-ui/react'
export default function Demo() {
return (
<Image
src="/sample.webp"
alt="夏日午后的坡道"
lazy={false}
eager
ratio={16 / 9}
className="w-full max-w-md rounded-md"
/>
)
}
空态与失败
没有 src 时渲染 empty 属性。图片加载失败且没有可用的回退地址时,渲染 error 属性。
加载完成
没有图片
加载失败
import { ImageOff } from 'lucide-react'
import { Center, Image, Inline, Stack, Text } from '@hina-ui/react'
export default function Demo() {
return (
<Inline align="start" className="gap-4">
<Stack gap="xs" className="w-40">
<Text tone="muted" size="sm">
加载完成
</Text>
<Image src="/sample.webp" alt="夏日午后的坡道" ratio={1} className="rounded-md" />
</Stack>
<Stack gap="xs" className="w-40">
<Text tone="muted" size="sm">
没有图片
</Text>
<Image
ratio={1}
className="bg-inset rounded-md"
empty={
<Center className="text-muted size-full">
<ImageOff className="size-6" />
</Center>
}
/>
</Stack>
<Stack gap="xs" className="w-40">
<Text tone="muted" size="sm">
加载失败
</Text>
<Image
src="/missing.webp"
alt=""
ratio={1}
className="bg-inset rounded-md"
error={
<Center className="text-muted size-full">
<Text size="sm">图片不可用</Text>
</Center>
}
/>
</Stack>
</Inline>
)
}
回退
src 加载失败后改用 fallback。回退地址同样失败时交给 error 属性,并调用 onError。
import { Image } from '@hina-ui/react'
export default function Demo() {
return (
<Image
src="/missing.webp"
fallback="/sample.webp"
alt="夏日午后的坡道"
ratio={16 / 9}
className="w-full max-w-md rounded-md"
/>
)
}
解析地址
组件只接收 src,不关心它是完整地址还是对象存储中的键。用 ImageResolverProvider 包住应用或页面,通过 resolver 提供一个函数,由它把 src 变成最终地址;处理参数之类的细节由这个函数自行掌握,不必经过组件。没有提供解析器时,src 按原样使用。函数还会收到第二个参数说明用途:页面上的图片为 'image',预览时放大查看的大图为 'preview'。
这里的解析器为地址补充查询串,真实应用会将地址指向 CDN 与图片处理服务。
'use client'
import { Image, ImageResolverProvider, Stack, Text, type ImageResolver } from '@hina-ui/react'
const resolver: ImageResolver = src => `${src}?preset=banner&quality=82`
export default function Demo() {
return (
<ImageResolverProvider resolver={resolver}>
<Stack className="w-full max-w-md">
<Image src="/sample.webp" alt="夏日午后的坡道" ratio={16 / 9} className="rounded-md" />
<Text tone="muted" size="sm">
这里的解析器为地址补充查询串,真实应用会将地址指向 CDN 与图片处理服务。
</Text>
</Stack>
</ImageResolverProvider>
)
}
预览
设置 preview 后,图片可以点击放大查看:图片从页面上的原位放大至屏幕中央,关闭时缩回原位。预览支持缩放、拖动、旋转与下载,向下拖动图片也可以关闭。
小图和大图可以是两种规格:解析器按用途给出各自的地址,或者直接把大图地址传给 preview。打开时先显示页面上的小图,大图就绪后替换。
'use client'
import { Image, ImageResolverProvider, type ImageResolver } from '@hina-ui/react'
const resolver: ImageResolver = (src, variant) =>
`https://imagesp.yurari.moe/${src}?w=${variant === 'preview' ? 2000 : 600}&f=webp&fit=scale-down&q=85`
export default function Demo() {
return (
<ImageResolverProvider resolver={resolver}>
<Image
src="images/1ec4bca1-0674-4da6-b1a2-7433f1d79df5.webp"
alt="《街角魔族》第一卷的封面"
preview
className="size-48 rounded-md"
/>
</ImageResolverProvider>
)
}
需要独立控制预览时,可以直接使用 Lightbox。
预览原始尺寸
通过 previewSize 提供最终预览图片的原始像素宽高。存在独立的 preview 地址时,尺寸对应解析后的大图;否则对应当前图片。组件据此提前计算初始适配尺寸和缩放上限,打开时一次展开到位,大图加载完成后只替换画质。
宽高必须同时是有限正数。有效的显式尺寸始终优先于加载结果;未传入或无效时,继续自动获取尺寸。previewSize 只影响预览,页面外框仍由 className、style 和 ratio 控制。
下例对比自动获取尺寸与预先提供尺寸,两张图片使用同一份 320 × 180 缩略图和 1200 × 675 大图。
自动获取尺寸
预先提供尺寸
import { Image, Inline, Stack, Text } from '@hina-ui/react'
const image = {
thumbnail: '/sample-thumbnail.webp',
original: '/sample.webp',
width: 1200,
height: 675,
}
export default function Demo() {
return (
<Inline align="start" className="gap-6">
<Stack gap="sm" className="w-64">
<Text size="sm" tone="muted">
自动获取尺寸
</Text>
<Image
src={image.thumbnail}
preview={image.original}
alt="夏日午后的坡道"
ratio={16 / 9}
className="rounded-xl"
/>
</Stack>
<Stack gap="sm" className="w-64">
<Text size="sm" tone="muted">
预先提供尺寸
</Text>
<Image
src={image.thumbnail}
preview={image.original}
previewSize={{ width: image.width, height: image.height }}
alt="夏日午后的坡道"
ratio={16 / 9}
className="rounded-xl"
/>
</Stack>
</Inline>
)
}
分组
把多张图片放进 ImageGroup,点击任意一张后可以在整组之间切换,顺序与页面上的顺序一致。开启 loop 后翻页首尾相接。
'use client'
import {
Image,
ImageGroup,
ImageResolverProvider,
Inline,
type ImageResolver,
} from '@hina-ui/react'
const resolver: ImageResolver = (src, variant) =>
`https://imagesp.yurari.moe/${src}?w=${variant === 'preview' ? 2000 : 600}&f=webp&fit=scale-down&q=85`
const pictures = [
{ src: 'galgame/10509/y71oty0w_2.jpg', alt: '《宿星的女友 3》的截图', className: 'w-48' },
{
src: 'galgame/10003/orhsttk4_2.jpg',
alt: '《抬头看看吧,看那天上的繁星》的封面',
className: 'w-20',
},
{ src: 'galgame/10012/co4a4kyg_2.png', alt: '《ONE.》的主视觉', className: 'w-20' },
{
src: 'images/1ec4bca1-0674-4da6-b1a2-7433f1d79df5.webp',
alt: '《街角魔族》第一卷的封面',
className: 'w-20',
},
{
src: 'images/74b0eddb-819f-4ee8-b406-fa096943a5b9.webp',
alt: '《抬头看看吧,看那天上的繁星 FINE DAYS》的封面',
className: 'w-40',
},
]
export default function Demo() {
return (
<ImageResolverProvider resolver={resolver}>
<ImageGroup loop>
<Inline>
{pictures.map(picture => (
<Image
key={picture.src}
src={picture.src}
alt={picture.alt}
preview
className={`h-28 rounded-md ${picture.className}`}
/>
))}
</Inline>
</ImageGroup>
</ImageResolverProvider>
)
}
行为
- 预览的开关动画保留图片及外层裁剪容器的圆角;多图布局按每张图片实际接触的裁剪边界分别计算四角。
- 外框默认撑满容器宽度,开启或关闭
preview时一致;显式宽度类名可以覆盖默认值。 - 默认懒加载:交叉观察器观察外框,图片距视口不足
rootMargin时才带上地址开始请求,该值默认为 200 像素。 - 服务端渲染阶段观察器尚未介入,懒加载的图片先不带地址。首屏图片应当关闭
lazy并开启eager。 - 骨架铺满外框,
fit为contain时留出的空白同样被覆盖。图片解码完成后,在骨架上层以 300ms 淡入,骨架同时以 200ms 淡出,两者均使用 ease-out 曲线。 - 懒加载的图片在淡入开始前保持透明;关闭骨架后,图片仍然淡入。
- 关闭
lazy的图片位于骨架上层,浏览器完成绘制即可见,无需等待脚本;其下方的骨架直接移除,不做淡出。 - 浏览器不支持交叉观察器时,组件挂载后立即加载,不会使图片始终无法显示。
- 组件未声明的其余特性会落到
img上,因此sizes、srcset之类照常可用。 - 更换
src会重置回退状态,新图片从自己的地址开始加载,而不是沿用上一张的回退地址。 - 预览打开期间页面不能滚动,焦点留在预览层内;关闭后焦点回到图片。
- 向下拖动时图片随指针下移并缩小,背景逐渐透出页面;松开时拖动距离不足则弹回原处。
- 双指捏合以两指中点为中心缩放,滚轮以指针所指的点为中心缩放。初次打开不放大小图,双击目标由图片尺寸决定,手动放大上限为原图的 2 倍;具体规则见 Lightbox 的尺寸与缩放。捏合超出上下限时阻力逐渐增大,松开后弹回限值。
- 放大之后才能拖动查看,拖到边缘有阻力,松开后弹回;快速滑动后画面继续滑行并逐渐停下,到达边界时回弹。放大状态下向下拖动只平移画面,不会关闭。
- 切换图片时上一张的缩放、位置与旋转复位;组内图片增删后,预览中的图片随之更新。只有一张图片,或者未开启
loop且已经到达两端时,继续拖动有阻力并回弹。 - 只为当前图片请求大图,切走后放弃请求,不会一次请求整组的大图。
- 关闭后再次打开时,缩放与位置都恢复初始状态。
- 系统开启减弱动态效果后,预览的打开、关闭、缩放、翻页与滑行都不再有过渡,直接切换到结果。
无障碍
alt直接落到img上。装饰性图片留空,屏幕阅读器会跳过它。- 骨架带
aria-hidden,加载状态由拥有这张图片的区域负责播报。 - 设置
preview的图片渲染为按钮,alt是它的可访问名称,也是预览层的标题;alt为空时开发环境会输出警告。 - 预览打开期间可用
←→键切换图片。缩略图栏中的每个按钮以图片的alt命名,当前项带aria-current。
API
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
src | string | — | 图片地址,会经过解析器 |
alt | string | '' | 替代文本 |
fallback | string | — | src 失败后改用的地址 |
fit | 'cover' | 'contain' | 'fill' | 'none' | 'scale-down' | 'cover' | 图片如何填满外框 |
ratio | number | — | 宽除以高,提前占位 |
lazy | boolean | true | 是否等接近视口再加载 |
rootMargin | string | '200px' | 提前多少距离开始加载 |
skeleton | boolean | true | 加载期间是否显示骨架 |
eager | boolean | false | 高优先级请求并同步解码 |
preview | boolean | string | false | 是否可以点击放大查看,传入字符串时作为大图地址 |
previewSize | { width: number; height: number } | — | 最终预览图片的原始像素尺寸,宽高须为有限正数 |
draggable | boolean | — | 图片是否可拖拽 |
className | string | — | 追加至外框的类名 |
style | CSSProperties | — | 外框的内联样式 |
imageClass | string | — | 追加至 img 的类名 |
imageStyle | CSSProperties | — | img 的内联样式 |
| 回调 | 参数 | 说明 |
|---|---|---|
onLoad | size: { width, height } | 图片加载完成 |
onError | — | 所有地址都已尝试过 |
| 属性 | 说明 |
|---|---|
skeletonContent | 替换内置的加载骨架 |
empty | 没有 src 时渲染 |
error | 加载失败时渲染 |