Button 按钮

触发一次操作的按钮。变体决定视觉样式,色调决定语义。

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>
  )
}
tsx

用法

import { Button } from '@hina-ui/react'
ts

同一界面中仅设置一个主操作,其余操作采用较轻的变体,取消类操作采用中性色调。

import { Button, Inline } from '@hina-ui/react'

export default function Demo() {
  return (
    <Inline>
      <Button>保存</Button>
      <Button variant="outline" tone="neutral">
        取消
      </Button>
    </Inline>
  )
}
tsx

示例

变体

共五种视觉样式,从实心到纯文字,视觉重量依次减轻。link 只保留文字与下划线,没有底色与按下效果。

variant
tone
<Button>按钮</Button>
tsx

色调

accent 用于主操作,neutral 用于次要操作与取消,danger 用于不可撤销的操作。色调与变体相互独立。

tone
variant
<Button>确认</Button>
tsx

尺寸

共三种尺寸,高度与内边距随密度缩放。

size
variant
<Button>按钮</Button>
tsx

密度

密度在容器上声明,对容器内的所有控件生效。上排为默认密度,下排为紧凑密度。

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>
  )
}
tsx

图标

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>
  )
}
tsx

仅图标

仅包含图标时使用 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>
  )
}
tsx

按钮组

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>
  )
}
tsx

状态

设置 loading 显示加载指示器并阻止点击,设置 disabled 禁用按钮。

loading
disabled
variant
<Button>保存</Button>
tsx

加载指示器优先替换前置图标;无前置图标时替换后置图标;两者均无时居中覆盖于文字之上。此时文字仅设为透明而保留在原位,按钮宽度保持不变。

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>
  )
}
tsx

提交中

点击后进入加载状态,期间不再响应点击,文案随之切换。

'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>
  )
}
tsx

第三方登录

设置 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>
  )
}
tsx

形状与宽度

设置 pill 使按钮呈胶囊形,设置 block 使其占满容器宽度。

pill
block
size
<Button>继续</Button>
tsx

自定义样式

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>
  )
}
tsx

全局覆盖

按钮的颜色、圆角与焦点环均取自语义变量。在任意作用域中重新声明这些变量,该作用域内按钮的外观会随之改变。

.brand-purple {
  --hn-accent: oklch(0.55 0.22 300);
  --hn-accent-on: #ffffff;
  --hn-focus-ring: oklch(0.5 0.22 300);
}
css

样式参考

尺寸变量

变量默认紧凑
--hn-control-h-sm1.75rem1.5rem
--hn-control-h-md2.25rem1.875rem
--hn-control-h-lg2.75rem2.25rem
--hn-control-px-sm0.625rem0.375rem
--hn-control-px-md1rem0.5rem
--hn-control-px-lg1.25rem0.75rem
--hn-control-gap0.375rem0.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
后置图标,仅在无前置图标时被替换