import { Send } from 'lucide-react'
import { Button, Inline } from '@hina-ui/react'
export default function Demo() {
return (
<Inline>
<Button variant="ghost" tone="neutral">
取消
</Button>
<Button variant="outline" tone="neutral">
存为草稿
</Button>
<Button icon={<Send />}>发布</Button>
</Inline>
)
}
用法
import { Button } from '@hina-ui/react'
同一界面中仅设置一个主操作,其余操作采用较轻的变体,取消类操作采用中性色调。
import { Button, Inline } from '@hina-ui/react'
export default function Demo() {
return (
<Inline>
<Button>保存</Button>
<Button variant="outline" tone="neutral">
取消
</Button>
</Inline>
)
}
示例
变体
共五种视觉样式,从实心到纯文字,视觉重量依次减轻。link 只保留文字与下划线,没有底色与按下效果。
<Button>按钮</Button>色调
accent 用于主操作,neutral 用于次要操作与取消,danger 用于不可撤销的操作。色调与变体相互独立。
<Button>确认</Button>尺寸
共三种尺寸,高度与内边距随密度缩放。
<Button>按钮</Button>密度
密度在容器上声明,对容器内的所有控件生效。上排为默认密度,下排为紧凑密度。
import { Button, Inline, Stack } from '@hina-ui/react'
export default function Demo() {
return (
<Stack>
<Inline>
<Button size="sm">Small</Button>
<Button size="md">Medium</Button>
<Button size="lg">Large</Button>
</Inline>
<Inline data-density="compact">
<Button size="sm">Small</Button>
<Button size="md">Medium</Button>
<Button size="lg">Large</Button>
</Inline>
</Stack>
)
}
图标
icon 用于放置前置图标,trailing 用于放置后置图标。
import { ArrowRight, Plus, Settings } from 'lucide-react'
import { Button, IconButton, Inline } from '@hina-ui/react'
export default function Demo() {
return (
<Inline>
<Button icon={<Plus />}>新建</Button>
<Button variant="outline" tone="neutral" trailing={<ArrowRight />}>
下一步
</Button>
<IconButton label="设置" variant="outline">
<Settings />
</IconButton>
</Inline>
)
}
仅图标
仅包含图标时使用 IconButton。该组件要求提供 label,该值同时用作无障碍名称与悬停提示文字。
import { Bookmark, Check, Share2, Trash2 } from 'lucide-react'
import { IconButton, Inline } from '@hina-ui/react'
export default function Demo() {
return (
<Inline>
<IconButton label="收藏">
<Bookmark />
</IconButton>
<IconButton label="分享" variant="outline">
<Share2 />
</IconButton>
<IconButton label="确认" variant="solid" tone="accent">
<Check />
</IconButton>
<IconButton label="删除" variant="soft" tone="danger">
<Trash2 />
</IconButton>
</Inline>
)
}
按钮组
ButtonGroup 将多个按钮拼接为一个整体,相邻的圆角与边框会自动合并。设置 divider 可以在相邻按钮之间添加分隔线。
import { AlignCenter, AlignLeft, AlignRight } from 'lucide-react'
import { Button, ButtonGroup, IconButton, Stack } from '@hina-ui/react'
export default function Demo() {
return (
<Stack align="center">
<ButtonGroup label="对齐方式">
<IconButton label="左对齐" variant="outline" tone="neutral">
<AlignLeft />
</IconButton>
<IconButton label="居中" variant="outline" tone="neutral">
<AlignCenter />
</IconButton>
<IconButton label="右对齐" variant="outline" tone="neutral">
<AlignRight />
</IconButton>
</ButtonGroup>
<ButtonGroup label="视图" divider>
<Button variant="soft" tone="neutral">
列表
</Button>
<Button variant="soft" tone="neutral">
网格
</Button>
<Button variant="soft" tone="neutral">
时间线
</Button>
</ButtonGroup>
</Stack>
)
}
状态
设置 loading 显示加载指示器并阻止点击,设置 disabled 禁用按钮。
<Button>保存</Button>加载指示器优先替换前置图标;无前置图标时替换后置图标;两者均无时居中覆盖于文字之上。此时文字仅设为透明而保留在原位,按钮宽度保持不变。
import { ArrowRight, Save } from 'lucide-react'
import { Button, Inline } from '@hina-ui/react'
export default function Demo() {
return (
<Inline>
<Button loading>保存</Button>
<Button loading variant="soft" icon={<Save />}>
保存
</Button>
<Button loading variant="outline" tone="neutral" trailing={<ArrowRight />}>
下一步
</Button>
</Inline>
)
}
提交中
点击后进入加载状态,期间不再响应点击,文案随之切换。
'use client'
import { useState } from 'react'
import { Upload } from 'lucide-react'
import { Button } from '@hina-ui/react'
export default function Demo() {
const [uploading, setUploading] = useState(false)
function upload() {
setUploading(true)
setTimeout(() => setUploading(false), 2000)
}
return (
<Button loading={uploading} onClick={upload} icon={<Upload />}>
{uploading ? '上传中…' : '上传文件'}
</Button>
)
}
第三方登录
设置 block 使按钮占满容器宽度,品牌图标通过 icon 传入。
import { Button, Stack } from '@hina-ui/react'
import { RawIcon } from '../../../components/BrandIcon'
const appleSvg =
'<svg fill="currentColor" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Apple</title><path d="M11.932 6.908c.95 0 2.727-1.291 4.595-1.1.782.032 2.976.316 4.388 2.38-.113.069-2.622 1.528-2.593 4.565.034 3.617 3.166 4.828 3.221 4.85-.029.086-.506 1.723-1.658 3.416-1.002 1.463-2.039 2.919-3.675 2.95-1.606.03-2.125-.955-3.96-.955s-2.409.923-3.931.984c-1.581.06-2.78-1.58-3.79-3.037-2.065-2.98-3.64-8.422-1.527-12.087 1.051-1.824 2.93-2.98 4.969-3.009 1.549-.032 3.011 1.043 3.96 1.043zM16.552 0c.153 1.407-.411 2.817-1.251 3.833-.837 1.013-2.214 1.804-3.555 1.7-.185-1.378.495-2.814 1.27-3.712C13.883.805 15.346.05 16.553 0z"></path></svg>'
const githubSvg =
'<svg fill="currentColor" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Github</title><path d="M12 0c6.63 0 12 5.276 12 11.79-.001 5.067-3.29 9.567-8.175 11.187-.6.118-.825-.25-.825-.56 0-.398.015-1.665.015-3.242 0-1.105-.375-1.813-.81-2.181 2.67-.295 5.475-1.297 5.475-5.822 0-1.297-.465-2.344-1.23-3.169.12-.295.54-1.503-.12-3.125 0 0-1.005-.324-3.3 1.209a11.32 11.32 0 00-3-.398c-1.02 0-2.04.133-3 .398-2.295-1.518-3.3-1.209-3.3-1.209-.66 1.622-.24 2.83-.12 3.125-.765.825-1.23 1.887-1.23 3.169 0 4.51 2.79 5.527 5.46 5.822-.345.294-.66.81-.765 1.577-.69.31-2.415.81-3.495-.973-.225-.354-.9-1.223-1.845-1.209-1.005.015-.405.56.015.781.51.28 1.095 1.327 1.23 1.666.24.663 1.02 1.93 4.035 1.385 0 .988.015 1.916.015 2.196 0 .31-.225.664-.825.56C3.303 21.374-.003 16.867 0 11.791 0 5.276 5.37 0 12 0z"></path></svg>'
const googleSvg =
'<svg height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Google</title><path d="M23 12.245c0-.905-.075-1.565-.236-2.25h-10.54v4.083h6.186c-.124 1.014-.797 2.542-2.294 3.569l-.021.136 3.332 2.53.23.022C21.779 18.417 23 15.593 23 12.245z" fill="#4285F4"></path><path d="M12.225 23c3.03 0 5.574-.978 7.433-2.665l-3.542-2.688c-.948.648-2.22 1.1-3.891 1.1a6.745 6.745 0 01-6.386-4.572l-.132.011-3.465 2.628-.045.124C4.043 20.531 7.835 23 12.225 23z" fill="#34A853"></path><path d="M5.84 14.175A6.65 6.65 0 015.463 12c0-.758.138-1.491.361-2.175l-.006-.147-3.508-2.67-.115.054A10.831 10.831 0 001 12c0 1.772.436 3.447 1.197 4.938l3.642-2.763z" fill="#FBBC05"></path><path d="M12.225 5.253c2.108 0 3.529.892 4.34 1.638l3.167-3.031C17.787 2.088 15.255 1 12.225 1 7.834 1 4.043 3.469 2.197 7.062l3.63 2.763a6.77 6.77 0 016.398-4.572z" fill="#EB4335"></path></svg>'
const providers = [
{ svg: googleSvg, label: '使用 Google 登录' },
{ svg: githubSvg, label: '使用 GitHub 登录' },
{ svg: appleSvg, label: '使用 Apple 登录' },
]
export default function Demo() {
return (
<Stack gap="sm" className="w-full max-w-xs">
{providers.map(provider => (
<Button
key={provider.label}
variant="soft"
tone="neutral"
block
pill
icon={<RawIcon svg={provider.svg} />}
>
{provider.label}
</Button>
))}
</Stack>
)
}
形状与宽度
设置 pill 使按钮呈胶囊形,设置 block 使其占满容器宽度。
<Button>继续</Button>作为链接
通过 as 将按钮渲染为 a 元素或者 next/link 的 Link,外观与交互保持一致。
import { ArrowUpRight } from 'lucide-react'
import { Button, Inline } from '@hina-ui/react'
export default function Demo() {
return (
<Inline>
<Button
as="a"
href="https://www.hikarinagi.com"
target="_blank"
rel="noreferrer"
variant="outline"
trailing={<ArrowUpRight />}
>
访问 Hikarinagi
</Button>
<Button
as="a"
href="https://www.hikarinagi.com"
target="_blank"
rel="noreferrer"
variant="link"
>
了解更多
</Button>
</Inline>
)
}
仅需链接外观而不需要按钮语义时,应使用 Link 组件,而非 variant="link"。
如果目标组件需要自行渲染根元素,改用 asChild:按钮不渲染自身,而是将类名与行为合并至唯一的子元素。Server Component 不能通过 as 把 Link 这类组件传给按钮,此时同样使用 asChild。
此模式下,图标、波纹和加载指示等内容由子元素负责。loading 仍会设置忙碌状态并拦截点击。
import Link from 'next/link'
import { Button } from '@hina-ui/react'
export function GetStarted() {
return (
<Button asChild>
<Link href="/guide/installation">开始使用</Link>
</Button>
)
}
自定义样式
Tailwind 类名
传入的 className 经 tailwind-merge 合并,同一属性以后声明者为准,无需 !important。
import { Button, Inline } from '@hina-ui/react'
export default function Demo() {
return (
<Inline>
<Button className="rounded-none px-8">直角</Button>
<Button variant="outline" tone="neutral" className="border-dashed">
虚线边框
</Button>
<Button className="bg-purple-600 text-white hover:bg-purple-700">紫色</Button>
</Inline>
)
}
全局覆盖
按钮的颜色、圆角与焦点环均取自语义变量。在任意作用域中重新声明这些变量,该作用域内按钮的外观会随之改变。
.brand-purple {
--hn-accent: oklch(0.55 0.22 300);
--hn-accent-on: #ffffff;
--hn-focus-ring: oklch(0.5 0.22 300);
}
样式参考
尺寸变量
| 变量 | 默认 | 紧凑 |
|---|---|---|
--hn-control-h-sm | 1.75rem | 1.5rem |
--hn-control-h-md | 2.25rem | 1.875rem |
--hn-control-h-lg | 2.75rem | 2.25rem |
--hn-control-px-sm | 0.625rem | 0.375rem |
--hn-control-px-md | 1rem | 0.5rem |
--hn-control-px-lg | 1.25rem | 0.75rem |
--hn-control-gap | 0.375rem | 0.25rem |
紧凑密度适用于后台表格一类的密集界面。密度不随设备类型变化,如果触摸屏上需要更大的点击目标,应当显式使用默认密度。
状态属性
渲染为原生 button 时直接使用原生的 disabled。渲染为其他元素时不具备原生的禁用语义,改由以下属性表示状态。
| 属性 | 含义 |
|---|---|
data-loading | 加载中 |
aria-busy | 加载中,供屏幕阅读器播报 |
data-disabled | 不可用 |
aria-disabled | 不可用,供屏幕阅读器播报 |
tabIndex={-1} | 不可用,键盘不会聚焦到该按钮 |
加载中的按钮同样视为不可用。
悬停与按下
- 焦点环仅在键盘聚焦时出现,鼠标点击不留下轮廓。
- 悬停与按下时会叠加一层半透明色,而不是替换底色;触摸设备上不会残留悬停状态。
- 波纹自按下的位置扩散,此时半透明层的按下效果不再叠加。
link变体没有波纹。
无障碍
- 仅包含图标的按钮必须提供无障碍名称,未提供时开发环境将输出告警。建议直接使用
IconButton。 - 加载中的按钮带有
aria-busy,屏幕阅读器会播报忙碌状态。
API
Props
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
as | string | Component | 'button' | 渲染的元素或组件 |
asChild | boolean | false | 不渲染自身,合并至唯一的子元素 |
variant | 'solid' | 'soft' | 'outline' | 'ghost' | 'link' | 'solid' | 视觉样式 |
tone | 'accent' | 'neutral' | 'danger' | 'accent' | 语义色调 |
size | 'sm' | 'md' | 'lg' | 'md' | 尺寸 |
type | 'button' | 'submit' | 'reset' | 'button' | 原生 button 类型 |
iconOnly | boolean | false | 是否为正方形且仅包含图标 |
block | boolean | false | 是否占满容器宽度 |
pill | boolean | false | 是否呈胶囊形 |
loading | boolean | false | 是否处于加载状态 |
disabled | boolean | false | 是否禁用 |
ripple | boolean | true | 是否启用按下波纹,link 变体不生效 |
className | string | — | 追加至根元素的类名 |
内容属性
| 属性 | 说明 |
|---|---|
children | 按钮内容,多个子元素之间自动隔开 |
icon | 前置图标,加载时被指示器替换 |
trailing | 后置图标,仅在无前置图标时被替换 |