FloatButton 浮动按钮

将一个常用操作放在页面或容器的边角,滚动时仍能找到。

工作笔记

本周设计回顾

下一版组件清单

'use client'

import { useState } from 'react'
import { Plus } from 'lucide-react'
import {
  Button,
  Card,
  Dialog,
  FloatButton,
  FormField,
  Heading,
  Input,
  Stack,
  Text,
} from '@hina-ui/react'

export default function Demo() {
  const [open, setOpen] = useState(false)
  const [title, setTitle] = useState('')
  const [notes, setNotes] = useState(['本周设计回顾', '下一版组件清单'])

  function create() {
    if (!title.trim()) return
    setNotes([...notes, title.trim()])
    setTitle('')
    setOpen(false)
  }

  return (
    <Card className="relative min-h-72 w-full max-w-md pb-24">
      <Stack gap="lg">
        <Heading level={3} size="base">
          工作笔记
        </Heading>
        <Stack gap="sm">
          {notes.map((note, index) => (
            <Text key={index} className="border-line border-b pb-3">
              {note}
            </Text>
          ))}
        </Stack>
      </Stack>
      <FloatButton position="absolute" label="新建笔记" onClick={() => setOpen(true)}>
        <Plus />
      </FloatButton>
      <Dialog
        open={open}
        onOpenChange={setOpen}
        title="新建笔记"
        description="为新的笔记填写标题。"
        renderContent={() => (
          <FormField label="笔记标题">
            <Input
              value={title}
              onValueChange={setTitle}
              placeholder="例如:交互细节"
              onKeyDown={event => {
                if (event.key !== 'Enter') return
                event.preventDefault()
                create()
              }}
            />
          </FormField>
        )}
        renderFooter={() => (
          <>
            <Button variant="soft" tone="neutral" onClick={() => setOpen(false)}>
              取消
            </Button>
            <Button disabled={!title.trim()} onClick={create}>
              创建
            </Button>
          </>
        )}
      />
    </Card>
  )
}
tsx

用法

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

默认固定在视口的逻辑右下角。通过 label 提供操作名称,children 放图标。浮动按钮适合新建、帮助这类跨内容区域的常用操作;行内操作继续使用 Button 或 IconButton。

<FloatButton label="新建笔记" onClick={createNote}>
  <Plus />
</FloatButton>
tsx

整页使用时放在应用壳外层,避免祖先的 transform 改变固定定位的参照。组件不通过 portal 渲染到 body,保留当前位置的主题、方向和组件上下文。上面的演示使用 position="absolute",将按钮限制在预览面板内。

示例

外观

支持实底、浅底和描边,shape="square" 使用圆角方形。图标按钮沿用现有 Tooltip;应用需提供 TooltipProvider,AppShell 已包含它。label 始终作为无障碍名称,不依赖 Tooltip。

悬停或键盘聚焦可以查看操作名称

'use client'

import { useState } from 'react'
import { HelpCircle, Pencil, Plus } from 'lucide-react'
import { FloatButton, Flex, Stack, Text } from '@hina-ui/react'

export default function Demo() {
  const [result, setResult] = useState('悬停或键盘聚焦可以查看操作名称')

  return (
    <Stack align="center" gap="lg">
      <Flex wrap justify="center" gap="lg">
        <FloatButton
          position="static"
          label="新建项目"
          onClick={() => setResult('已选择:新建项目')}
        >
          <Plus />
        </FloatButton>
        <FloatButton
          position="static"
          label="编辑项目"
          variant="soft"
          shape="square"
          onClick={() => setResult('已选择:编辑项目')}
        >
          <Pencil />
        </FloatButton>
        <FloatButton
          position="static"
          label="帮助中心"
          variant="outline"
          tone="neutral"
          onClick={() => setResult('已选择:帮助中心')}
        >
          <HelpCircle />
        </FloatButton>
      </Flex>
      <Text role="status" size="sm" tone="muted">
        {result}
      </Text>
    </Stack>
  )
}
tsx

尺寸与文字

sm、md、lg 分别对应 40、48、56px 的默认高度。extended 将 label 显示在图标旁,此时不会重复显示 Tooltip。长文案在可用宽度内截断。

文字按钮使用同一高度,宽度随名称展开

'use client'

import { useState } from 'react'
import { Plus } from 'lucide-react'
import { FloatButton, Flex, Stack, Text } from '@hina-ui/react'

export default function Demo() {
  const [result, setResult] = useState('文字按钮使用同一高度,宽度随名称展开')

  return (
    <Stack align="center" gap="lg">
      <Flex wrap align="center" justify="center" gap="lg">
        {(['sm', 'md', 'lg'] as const).map(size => (
          <FloatButton
            key={size}
            position="static"
            size={size}
            label={`新建项目(${size})`}
            onClick={() => setResult(`已选择:${size}`)}
          >
            <Plus />
          </FloatButton>
        ))}
        <FloatButton
          position="static"
          extended
          label="新建项目"
          onClick={() => setResult('已选择:新建项目')}
        >
          <Plus />
        </FloatButton>
      </Flex>
      <Text role="status" size="sm" tone="muted">
        {result}
      </Text>
    </Stack>
  )
}
tsx

定位与方向

placement 使用逻辑方向,start / end 随 RTL 切换。absolute 相对最近的定位祖先,static 参与正常布局,适合与 Stack 组合成一组操作。offset 控制边距;固定定位时还会避开设备安全区。

start / end 随书写方向切换

'use client'

import { useState } from 'react'
import { Plus } from 'lucide-react'
import { Card, Center, FloatButton, FormField, Stack, Switch, Text } from '@hina-ui/react'

const placements = ['top-start', 'top-end', 'bottom-start', 'bottom-end'] as const

export default function Demo() {
  const [rtl, setRtl] = useState(false)
  const [result, setResult] = useState('start / end 随书写方向切换')

  return (
    <Stack className="w-full max-w-md">
      <FormField label="从右向左" orientation="horizontal">
        <Switch checked={rtl} onCheckedChange={setRtl} />
      </FormField>
      <Card dir={rtl ? 'rtl' : 'ltr'} className="relative h-64" padded={false}>
        <Center className="h-full px-16">
          <Text role="status" size="sm" tone="muted" className="text-center">
            {result}
          </Text>
        </Center>
        {placements.map(placement => (
          <FloatButton
            key={placement}
            position="absolute"
            placement={placement}
            label={placement}
            offset={16}
            variant="soft"
            size="sm"
            onClick={() => setResult(placement)}
          >
            <Plus />
          </FloatButton>
        ))}
      </Card>
    </Stack>
  )
}
tsx

状态与显隐

visible 控制出现和退场,退场期间按钮不能交互。loading 保留按钮尺寸并阻止重复操作,disabled 禁用按钮。

显示和隐藏带有过渡

'use client'

import { useState } from 'react'
import { RefreshCw } from 'lucide-react'
import { Center, FloatButton, FormField, Stack, Switch, Text } from '@hina-ui/react'

export default function Demo() {
  const [visible, setVisible] = useState(true)
  const [loading, setLoading] = useState(false)
  const [disabled, setDisabled] = useState(false)
  const [result, setResult] = useState('显示和隐藏带有过渡')

  return (
    <Stack className="w-full max-w-xs">
      <FormField label="显示按钮" orientation="horizontal">
        <Switch checked={visible} onCheckedChange={setVisible} />
      </FormField>
      <FormField label="加载中" orientation="horizontal">
        <Switch checked={loading} onCheckedChange={setLoading} />
      </FormField>
      <FormField label="禁用" orientation="horizontal">
        <Switch checked={disabled} onCheckedChange={setDisabled} />
      </FormField>
      <Center className="h-24">
        <FloatButton
          position="static"
          label="同步资料"
          visible={visible}
          loading={loading}
          disabled={disabled}
          onClick={() => setResult('已执行:同步资料')}
        >
          <RefreshCw />
        </FloatButton>
      </Center>
      <Text role="status" size="sm" tone="muted" className="text-center">
        {result}
      </Text>
    </Stack>
  )
}
tsx

API

Props

属性
类型
默认值
说明
label
string
必填
操作名称,同时用于无障碍和 Tooltip
visible
boolean
true
显示按钮,变化时播放过渡
position
'fixed' | 'absolute' | 'static'
'fixed'
定位方式
placement
'top-start' | 'top-end' | 'bottom-start' | 'bottom-end'
'bottom-end'
定位角落,静态布局时无效
offset
number | string
6 × spacing
边距,数字单位为 px;固定定位取边距与安全区中的较大值
size
'sm' | 'md' | 'lg'
'md'
按钮尺寸
shape
'circle' | 'square'
'circle'
圆形或圆角方形
extended
boolean
false
显示文字标签
variant
'solid' | 'soft' | 'outline'
'solid'
按钮外观
tone
'accent' | 'neutral' | 'danger'
'accent'
按钮色调
tooltip
boolean
true
图标形态下显示 Tooltip
tooltipSide
'top' | 'right' | 'bottom' | 'left'
'top'
Tooltip 首选方向
loading
boolean
false
加载中并禁止操作
disabled
boolean
false
禁用操作
ripple
boolean
true
涟漪反馈
as
string | Component
'button'
实际操作元素或组件
type
'button' | 'submit' | 'reset'
'button'
按钮类型
className
string
—
按钮类名
style
CSSProperties
—
按钮样式

原生属性和事件透传到按钮,例如 id、form 和 onClick。

内容属性

属性
说明
children
操作图标;extended 时放在文字前面

Ref

名称
类型
说明
element
HTMLElement | undefined
当前操作节点
focus
() => void
聚焦可用的按钮