Toggle 切换按钮

可按下与松开的双态按钮。

'use client'

import { useState } from 'react'
import { Bold, Italic, Underline } from 'lucide-react'
import { Inline, Toggle } from '@hina-ui/react'

export default function Demo() {
  const [bold, setBold] = useState(true)
  const [italic, setItalic] = useState(false)
  const [underline, setUnderline] = useState(false)

  return (
    <Inline gap="xs">
      <Toggle value={bold} onValueChange={setBold} label="加粗" renderIcon={() => <Bold />} />
      <Toggle value={italic} onValueChange={setItalic} label="斜体" renderIcon={() => <Italic />} />
      <Toggle
        value={underline}
        onValueChange={setUnderline}
        label="下划线"
        renderIcon={() => <Underline />}
      />
    </Inline>
  )
}
tsx

用法

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

切换按钮是一个可以保持按下状态的按钮,用于加粗、收藏、筛选这类随时可以打开或者关闭的操作。value / onValueChange 绑定是否按下,children 是文字,renderIcon 放前置图标。外观取自 Button 的 ghost 与 outline 两种形态,按下后字色与墨转为品牌色。未声明的属性都会传给按钮元素。

显示全部作品

'use client'

import { useState } from 'react'
import { ListFilter } from 'lucide-react'
import { Stack, Text, Toggle } from '@hina-ui/react'

export default function Demo() {
  const [onlyDone, setOnlyDone] = useState(false)

  return (
    <Stack gap="sm" align="start">
      <Toggle value={onlyDone} onValueChange={setOnlyDone} renderIcon={() => <ListFilter />}>
        仅看已完结
      </Toggle>
      <Text tone="muted" size="sm">
        {onlyDone ? '只显示已完结的作品' : '显示全部作品'}
      </Text>
    </Stack>
  )
}
tsx

它与 Switch 的区别在于场合:开关表示一项设置,总是带文字说明,独立成行;切换按钮是一个动作,常常只有图标,成排出现在工具栏里。与 Chip 的可选中形态相比,Chip 是行内的条目,胶囊形且尺寸更小。

示例

图标型

label 提供无障碍名称。没有 children 文字时,按钮使用正方形的图标型尺寸;同时提供文字时,保留常规按钮内边距。在 TooltipProvider 内,label 还会作为 Tooltip 的文字提示。pressedIcon 给出按下时的图标,两个图标之间交叉淡变。

'use client'

import { useState } from 'react'
import { Bookmark, Star } from 'lucide-react'
import { Inline, Toggle } from '@hina-ui/react'

export default function Demo() {
  const [starred, setStarred] = useState(true)
  const [saved, setSaved] = useState(false)

  return (
    <Inline gap="xs">
      <Toggle
        value={starred}
        onValueChange={setStarred}
        label="收藏"
        pill
        renderIcon={() => <Star />}
        pressedIcon={<Star fill="currentColor" />}
      />
      <Toggle
        value={saved}
        onValueChange={setSaved}
        label="稍后再看"
        pill
        renderIcon={() => <Bookmark />}
        pressedIcon={<Bookmark fill="currentColor" />}
      />
    </Inline>
  )
}
tsx

工具栏

成排的图标型切换按钮各自持有一个布尔值,互不影响。需要恰好一项被选中时应使用 SegmentedControl。

'use client'

import { useState } from 'react'
import { Bold, Italic, Strikethrough, Underline } from 'lucide-react'
import { Inline, Toggle } from '@hina-ui/react'

export default function Demo() {
  const [marks, setMarks] = useState({
    bold: false,
    italic: false,
    underline: false,
    strike: false,
  })

  return (
    <Inline gap="xs">
      <Toggle
        value={marks.bold}
        onValueChange={bold => setMarks({ ...marks, bold })}
        label="加粗"
        size="sm"
        renderIcon={() => <Bold />}
      />
      <Toggle
        value={marks.italic}
        onValueChange={italic => setMarks({ ...marks, italic })}
        label="斜体"
        size="sm"
        renderIcon={() => <Italic />}
      />
      <Toggle
        value={marks.underline}
        onValueChange={underline => setMarks({ ...marks, underline })}
        label="下划线"
        size="sm"
        renderIcon={() => <Underline />}
      />
      <Toggle
        value={marks.strike}
        onValueChange={strike => setMarks({ ...marks, strike })}
        label="删除线"
        size="sm"
        renderIcon={() => <Strikethrough />}
      />
    </Inline>
  )
}
tsx

形态

ghost 为默认,outline 带发丝线边框,适合单独放在内容里。

'use client'

import { useState } from 'react'
import { Pin } from 'lucide-react'
import { Inline, Toggle } from '@hina-ui/react'

export default function Demo() {
  const [ghost, setGhost] = useState(true)
  const [outline, setOutline] = useState(true)

  return (
    <Inline gap="sm">
      <Toggle value={ghost} onValueChange={setGhost} variant="ghost" renderIcon={() => <Pin />}>
        置顶
      </Toggle>
      <Toggle
        value={outline}
        onValueChange={setOutline}
        variant="outline"
        renderIcon={() => <Pin />}
      >
        置顶
      </Toggle>
    </Inline>
  )
}
tsx

尺寸

size 有 sm、md、lg 三档,与 Button 逐档相同。

'use client'

import { Heart } from 'lucide-react'
import { Inline, Toggle } from '@hina-ui/react'

export default function Demo() {
  return (
    <Inline gap="sm" align="center">
      <Toggle size="sm" variant="outline" value renderIcon={() => <Heart />}>
        小号
      </Toggle>
      <Toggle size="md" variant="outline" value renderIcon={() => <Heart />}>
        中号
      </Toggle>
      <Toggle size="lg" variant="outline" value renderIcon={() => <Heart />}>
        大号
      </Toggle>
    </Inline>
  )
}
tsx

状态

disabled 禁用按钮,禁用时保留按下状态。

'use client'

import { Bold } from 'lucide-react'
import { Inline, Toggle } from '@hina-ui/react'

export default function Demo() {
  return (
    <Inline gap="sm">
      <Toggle variant="outline" disabled renderIcon={() => <Bold />}>
        已禁用
      </Toggle>
      <Toggle variant="outline" disabled value renderIcon={() => <Bold />}>
        禁用且按下
      </Toggle>
    </Inline>
  )
}
tsx

在表单中

放进 FormField 后,错误信息由字段渲染并关联到按钮;校验规则与提交交给 Form。切换按钮自带文字,字段不必再写标签;它的值决定其他字段是否必填时,规则写在对象层,再指定错误落在哪个字段。

'use client'

import { useState } from 'react'
import * as v from 'valibot'
import { Button, Form, FormField, Input, Text, Toggle } from '@hina-ui/react'

const schema = v.pipe(
  v.object({
    pinned: v.boolean(),
    title: v.string(),
  }),
  v.forward(
    v.check(input => !input.pinned || input.title.trim().length > 0, '置顶的公告需要标题'),
    ['title'],
  ),
)

export default function Demo() {
  const [values, setValues] = useState({ pinned: true, title: '' })
  const [saved, setSaved] = useState('')

  async function save(data: unknown) {
    await new Promise(resolve => setTimeout(resolve, 600))
    setSaved(JSON.stringify(data))
  }

  return (
    <Form values={values} rules={schema} className="w-80" onSubmit={save}>
      {({ submitting }) => (
        <>
          <FormField name="title" label="标题">
            <Input value={values.title} onValueChange={title => setValues({ ...values, title })} />
          </FormField>
          <FormField name="pinned">
            <Toggle
              value={values.pinned}
              onValueChange={pinned => setValues({ ...values, pinned })}
              variant="outline"
            >
              置顶
            </Toggle>
          </FormField>
          <Button type="submit" loading={submitting} className="self-start">
            发布
          </Button>
          {saved && (
            <Text tone="muted" size="sm">
              已发布:{saved}
            </Text>
          )}
        </>
      )}
    </Form>
  )
}
tsx

行为

  • 点击、空格或者 Enter 在按下与松开之间切换。
  • 按下态由品牌色的墨与字色表达,悬停与按压的墨叠加在其上。
  • 有 pressedIcon 时两个图标交叉淡变,不硬切。

无障碍

  • 根元素是带 aria-pressed 的按钮,读屏软件读作「切换按钮,已按下」。
  • 图标型必须提供 label;文字型默认以文字命名,也可通过 label 补充完整名称。

API

Props

属性
类型
默认值
说明
value
boolean
false
是否按下
label
string
—
无障碍名称;无 children 时使用图标型尺寸
tooltip
boolean
true
是否显示 label 对应的文字提示
side
'top' | 'right' | 'bottom' | 'left'
'top'
文字提示的位置
variant
'ghost' | 'outline'
'ghost'
形态
size
'sm' | 'md' | 'lg'
'md'
尺寸
pill
boolean
false
是否为胶囊形
disabled
boolean
false
是否禁用
ripple
boolean
true
是否显示按压波纹
className
string
—
追加至按钮元素的类名

内容属性

属性
参数
说明
children
—
文字
renderIcon
{ pressed: boolean }
前置图标
pressedIcon
—
按下时的前置图标

回调

回调
参数
说明
onValueChange
value: boolean
按下状态变化