Tooltip
深色提示气泡(夜樱里是浅色),带同色的小尾巴,给只有图标的按钮报名字。
tooltip-demo.tsx
import { DownloadIcon, HeartIcon, MagnetIcon, RssIcon } from "lucide-react"
import { Button } from "@/components/ui/button"
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"
const ACTIONS = [
{ label: "复制磁力链接", Icon: MagnetIcon },
{ label: "下载种子", Icon: DownloadIcon },
{ label: "订阅这个字幕组", Icon: RssIcon },
{ label: "加入追番", Icon: HeartIcon },
]
export default function TooltipDemo() {
return (
<TooltipProvider delayDuration={150}>
<div className="flex items-center gap-2">
{ACTIONS.map(({ label, Icon }) => (
<Tooltip key={label}>
<TooltipTrigger asChild>
<Button variant="secondary" size="icon" aria-label={label}>
<Icon />
</Button>
</TooltipTrigger>
<TooltipContent>{label}</TooltipContent>
</Tooltip>
))}
</div>
</TooltipProvider>
)
}安装
npx shadcn@latest add @anibt/tooltip第一次用之前:在 components.json 里登记 @anibt,再单独运行一次 npx shadcn@latest add @anibt/theme(见快速开始)。只有单独安装主题时,CLI 才会把 shadcn 的标准颜色换成白樱 / 夜樱;随组件带进来的主题只补上 AniBT 自己的变量和配方。
手动安装
- 1. 先装主题组件只读
@anibt/theme的 token 和配方,没有主题就没有样子。npx shadcn@latest add @anibt/theme - 2. 安装依赖
npm install radix-ui - 3. 把下面的文件放进项目components/ui/tooltip.tsx
"use client" import * as React from "react" import { Tooltip as TooltipPrimitive } from "radix-ui" import { cn } from "@/lib/utils" /** * Tooltip — the AniBT 「贴纸手账」 hint bubble. * * A's tip is the one dark thing on a light page (and the one light thing in * 夜樱): `--anibt-tip` under `--anibt-tip-ink`, 12/16, radius 6, the sticker * shadow, and a tail in its own colour pointing at the control. It names an * icon-only control or says one short fact; anything longer belongs in a * popover. * * Radix underneath, so hover, keyboard focus, Escape and the grace area all * behave. Compatibility with shadcn: * - `Tooltip` works without a `TooltipProvider` (it brings its own); put one * provider around a list so its tooltips share one delay. * - `TooltipTrigger` takes Base UI's `render={<button …/>}` as well as * `asChild`, and `TooltipProvider` / `Tooltip` take Base UI's `delay` as well * as Radix's `delayDuration`, so the `base-*` sidebar keeps working. */ const TooltipScope = React.createContext(false) type DelayProps = { /** Radix: ms before the tip opens. Default 0. */ delayDuration?: number /** Base UI's name for `delayDuration`. */ delay?: number } function TooltipProvider({ delayDuration, delay, children, ...props }: Omit<React.ComponentProps<typeof TooltipPrimitive.Provider>, "delayDuration"> & DelayProps) { return ( <TooltipScope.Provider value={true}> <TooltipPrimitive.Provider data-slot="tooltip-provider" delayDuration={delayDuration ?? delay ?? 0} {...props} > {children} </TooltipPrimitive.Provider> </TooltipScope.Provider> ) } function Tooltip({ delayDuration, delay, ...props }: Omit<React.ComponentProps<typeof TooltipPrimitive.Root>, "delayDuration"> & DelayProps) { const shared = React.useContext(TooltipScope) const root = ( <TooltipPrimitive.Root data-slot="tooltip" delayDuration={delayDuration ?? delay} {...props} /> ) // A tooltip inside a provider shares its delay; a lone one brings its own. return shared ? root : <TooltipProvider>{root}</TooltipProvider> } type TooltipTriggerProps = React.ComponentProps<typeof TooltipPrimitive.Trigger> & { /** * Base UI's form of `asChild`: the element to render as the trigger. It * takes the trigger's props; `children` go inside it. */ render?: React.ReactElement } function TooltipTrigger({ render, asChild, children, ...props }: TooltipTriggerProps) { if (render) { const child = children !== undefined ? React.cloneElement(render, undefined, children) : render return ( <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...{ asChild: true }} {...props}> {child} </TooltipPrimitive.Trigger> ) } return ( <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...{ asChild: asChild }} {...props}> {children} </TooltipPrimitive.Trigger> ) } function TooltipContent({ className, sideOffset = 6, children, ...props }: React.ComponentProps<typeof TooltipPrimitive.Content>) { return ( <TooltipPrimitive.Portal> <TooltipPrimitive.Content data-slot="tooltip-content" sideOffset={sideOffset} className={cn( "anibt-tip z-50 inline-flex w-fit max-w-[min(20rem,calc(100vw-2rem))] origin-(--radix-tooltip-content-transform-origin) items-center gap-1.5 text-balance", "animate-in fade-in-0 zoom-in-95 data-[side=bottom]:slide-in-from-top-1 data-[side=left]:slide-in-from-right-1 data-[side=right]:slide-in-from-left-1 data-[side=top]:slide-in-from-bottom-1 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95", // A key cap inside the tip (`⌘K`) takes the tip's ink, not a field's. "**:data-[slot=kbd]:h-4 **:data-[slot=kbd]:min-w-4 **:data-[slot=kbd]:bg-current/15 **:data-[slot=kbd]:text-current **:data-[slot=kbd]:shadow-none", className )} {...props} > {children} {/* A's tail: the tip's own colour, pointing at the control. */} <TooltipPrimitive.Arrow className="fill-anibt-tip" width={10} height={5} /> </TooltipPrimitive.Content> </TooltipPrimitive.Portal> ) } export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } export type { TooltipTriggerProps }
用法
用法
import { Button } from "@/components/ui/button"
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"
<Tooltip>
<TooltipTrigger asChild>
<Button variant="secondary" size="icon" aria-label="复制磁力链接">
<MagnetIcon />
</Button>
</TooltipTrigger>
<TooltipContent>复制磁力链接</TooltipContent>
</Tooltip>提示只说一句短话:给图标按钮报名字,或者补一个事实。更长的内容放进弹层。按钮自己也要有 aria-label——提示不是唯一的名字来源。
Tooltip 不需要外层的 TooltipProvider 也能用(它会自己带一个);一整个列表的提示共用一个 provider,延迟才一致。
方向与键帽
tooltip-sides.tsx
import { Button } from "@/components/ui/button"
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"
const SIDES = [
{ side: "top" as const, label: "上" },
{ side: "right" as const, label: "右" },
{ side: "bottom" as const, label: "下" },
{ side: "left" as const, label: "左" },
]
export default function TooltipSides() {
return (
<div className="flex flex-wrap items-center justify-center gap-2">
{SIDES.map(({ side, label }) => (
<Tooltip key={side}>
<TooltipTrigger asChild>
<Button variant="secondary" size="sm">
{label}
</Button>
</TooltipTrigger>
<TooltipContent side={side}>
搜索番剧 <kbd data-slot="kbd" className="anibt-kbd">⌘K</kbd>
</TooltipContent>
</Tooltip>
))}
</div>
)
}side、align、sideOffset 都是 Radix 的。提示里的键帽(data-slot="kbd")会自动换成气泡的颜色。
与 shadcn 的兼容
TooltipTrigger同时接受 Radix 的asChild和 Base UI 的render={<button … />},base-*风格的 sidebar 不用改。TooltipProvider/Tooltip同时接受delayDuration(Radix)和delay(Base UI)。
属性
TooltipContent
side"top" | "right" | "bottom" | "left"默认 "top"气泡在哪一边
sideOffsetnumber默认 6离控件的距离
align"start" | "center" | "end"默认 "center"沿边对齐
TooltipTrigger
asChildboolean默认 false把触发器交给唯一的子元素
renderReactElement默认 —Base UI 的写法:渲染成这个元素
TooltipProvider · Tooltip
delayDurationnumber默认 0多久后打开(毫秒)
delaynumber默认 —Base UI 对 delayDuration 的叫法
