故障文字
悬停时文字瞬间撕裂成错位的彩色切片,像信号中断一样,随后又干净地复原。
信号丢失
用纯 CSS 制作故障风格的文字悬停效果。把文字放在一个元素中,并把 data-text 设为同样的字符串,再用 ::before 和 ::after 配合 content: attr(data-text) 精确叠在上面。一层使用主色,另一层使用偏冷的浅色,静止时两层都不可见。在 :hover 和 :focus-visible 时显示这两层,并各自运行一个 600ms、steps(1) 的 keyframe,在四五个水平切片之间跳变 clip-path: inset(top% 0 bottom% 0),同时左右偏移几个像素,两层的节奏错开。底下的真实文字保持可读。给屏幕阅读器放一个视觉隐藏的 span,故障元素本身设为 aria-hidden。在 prefers-reduced-motion 下改为静态的双色错位。文字为“信号丢失”。
源代码
'use client'
import s from './demo.module.css'
export default function GlitchText({ labels }: { labels: Record<string, string> }) {
return (
<span className={s.wrap} tabIndex={0}>
<span className={s.sr}>{labels.text}</span>
<span className={s.glitch} data-text={labels.text} aria-hidden="true">
{labels.text}
</span>
</span>
)
}
.wrap {
border-radius: 6px;
outline: none;
cursor: default;
}
.wrap:focus-visible { outline: 2px solid var(--color-focus); outline-offset: 6px; }
.sr {
position: absolute;
width: 1px;
height: 1px;
overflow: hidden;
clip-path: inset(50%);
white-space: nowrap;
}
.glitch {
position: relative;
display: inline-block;
font: 800 clamp(2rem, 7vw, 2.6rem) / 1 var(--font-display);
letter-spacing: 0.04em;
color: var(--stage-ink);
}
.glitch::before,
.glitch::after {
content: attr(data-text);
position: absolute;
inset: 0;
opacity: 0;
}
.glitch::before { color: var(--stage-accent); }
.glitch::after { color: oklch(80% 0.1 220); }
.wrap:hover .glitch::before,
.wrap:focus-visible .glitch::before { opacity: 1; animation: slice-a 600ms steps(1) infinite; }
.wrap:hover .glitch::after,
.wrap:focus-visible .glitch::after { opacity: 1; animation: slice-b 600ms steps(1) infinite; }
@keyframes slice-a {
0% { clip-path: inset(10% 0 70% 0); translate: -4px 0; }
20% { clip-path: inset(55% 0 25% 0); translate: 3px 0; }
40% { clip-path: inset(30% 0 50% 0); translate: -2px 0; }
60% { clip-path: inset(80% 0 5% 0); translate: 4px 0; }
80% { clip-path: inset(0 0 85% 0); translate: -3px 0; }
}
@keyframes slice-b {
0% { clip-path: inset(65% 0 15% 0); translate: 3px 0; }
20% { clip-path: inset(5% 0 80% 0); translate: -4px 0; }
40% { clip-path: inset(40% 0 40% 0); translate: 2px 0; }
60% { clip-path: inset(20% 0 65% 0); translate: -3px 0; }
80% { clip-path: inset(75% 0 10% 0); translate: 4px 0; }
}
@media (prefers-reduced-motion: reduce) {
.wrap:hover .glitch::before,
.wrap:hover .glitch::after,
.wrap:focus-visible .glitch::before,
.wrap:focus-visible .glitch::after { animation: none; clip-path: inset(0 0 50% 0); }
.wrap:hover .glitch::before, .wrap:focus-visible .glitch::before { translate: -2px 0; }
.wrap:hover .glitch::after, .wrap:focus-visible .glitch::after { clip-path: inset(50% 0 0 0); translate: 2px 0; }
}