弹出提示
短暂停顿后,提示气泡带着一点弹性从图标按钮上方弹出,帮得上忙又不碍事。
用 CSS 制作弹出式的图标按钮提示。每个按钮设为 position: relative,内含一个 role="tooltip" 的提示元素,并通过按钮上的 aria-describedby 关联;提示位于按钮上方居中,用旋转的伪元素画一个小箭头。静止时提示为 opacity 0、transform: translate(-50%, 4px) scale(0.85),transform-origin 为底部中央,pointer-events none。在 :hover 和 :focus-visible 时,用 220ms、cubic-bezier(0.34, 1.56, 0.64, 1) 过渡到 opacity 1、translate(-50%, 0) scale(1),带一点回弹;并延迟 250ms 出现,避免指针划过工具栏时每个提示都闪一下。消失时不延迟,用 120ms 淡出。在 prefers-reduced-motion 下只淡入淡出,不缩放。提示文字为“加粗”“斜体”“添加链接”。
源代码
'use client'
import { useId } from 'react'
import s from './demo.module.css'
const ICONS = {
bold: <path d="M7 5h6a3.5 3.5 0 0 1 0 7H7zM7 12h7a3.5 3.5 0 0 1 0 7H7z" />,
italic: <path d="M14 5h-4M14 19h-4M13.5 5l-3 14" />,
link: <path d="M10 14a4 4 0 0 0 5.7 0l3-3a4 4 0 0 0-5.7-5.7l-1 1M14 10a4 4 0 0 0-5.7 0l-3 3a4 4 0 0 0 5.7 5.7l1-1" />,
}
export default function TooltipPop({ labels }: { labels: Record<string, string> }) {
const id = useId()
return (
<div className={s.bar} role="toolbar" aria-label={labels.toolbar}>
{(Object.keys(ICONS) as (keyof typeof ICONS)[]).map((k) => (
<button key={k} type="button" className={s.btn} aria-label={labels[k]} aria-describedby={`${id}-${k}`}>
<svg viewBox="0 0 24 24" aria-hidden="true">
{ICONS[k]}
</svg>
<span id={`${id}-${k}`} role="tooltip" className={s.tip}>
{labels[k]}
</span>
</button>
))}
</div>
)
}
.bar {
display: flex;
gap: 4px;
padding: 4px;
border: 1px solid var(--stage-rule);
border-radius: 12px;
background: var(--stage-surface);
}
.btn {
position: relative;
display: grid;
place-items: center;
width: 44px;
height: 44px;
border: 0;
border-radius: 8px;
background: transparent;
color: var(--stage-ink);
cursor: pointer;
}
.btn:hover { background: var(--stage-bg); }
.btn:focus-visible { outline: 2px solid var(--color-focus); outline-offset: 1px; }
.btn svg {
width: 18px;
height: 18px;
fill: none;
stroke: currentColor;
stroke-width: 2;
stroke-linecap: round;
stroke-linejoin: round;
}
.tip {
position: absolute;
left: 50%;
bottom: calc(100% + 10px);
padding: 6px 9px;
border-radius: 6px;
background: var(--stage-ink);
color: var(--stage-bg);
font: 500 var(--text-xs) / 1 var(--font-body);
white-space: nowrap;
pointer-events: none;
opacity: 0;
transform: translate(-50%, 4px) scale(0.85);
transform-origin: 50% 100%;
transition: opacity 120ms, transform 120ms;
}
.tip::after {
content: '';
position: absolute;
left: 50%;
top: 100%;
width: 8px;
height: 8px;
margin: -4px 0 0 -4px;
background: inherit;
rotate: 45deg;
}
.btn:hover .tip,
.btn:focus-visible .tip {
opacity: 1;
transform: translate(-50%, 0) scale(1);
transition: opacity 220ms 250ms, transform 220ms cubic-bezier(0.34, 1.56, 0.64, 1) 250ms;
}
@media (prefers-reduced-motion: reduce) {
.tip, .btn:hover .tip, .btn:focus-visible .tip { transform: translate(-50%, 0); }
}