Glitch Text
Глитч-надпись: слои-каналы в псевдоэлементах порезаны clip-path и дёргаются step-end всплесками. Дискретные прыжки, между всплесками — чистый текст.
Пропсы
Разобрано из кода записи: обязательные — без «?», у остальных показано значение по умолчанию.
| Проп | Тип | По умолчанию | Что это |
|---|---|---|---|
| text? | string | "ПОМЕХИ В КАНАЛЕ" | Текст надписи (он же уходит в data-атрибут для слоёв-каналов). |
| period? | number | 3.2 | Секунд на цикл (в цикле два коротких всплеска). |
| color? | string | "oklch(0.93 0.01 265)" | Цвет базовой надписи. |
Установка
npx shadcn@latest add https://motiva.pages.dev/r/glitch-text.jsonЧто нужно в проекте до установки
Файл
components.jsonв корнеИз него CLI узнаёт, куда класть файлы. Алиасы — в такой форме:
"aliases": { "components": "@/components", "lib": "@/lib" }Нет файла — установка не начнётся вовсе.
Алиас
@объявлен дваждыВ корневом
tsconfig.jsonи в конфиге сборщика. В Vite корневой tsconfig часто содержит толькоreferences— этого мало."paths": { "@/*": ["./src/*"] } // tsconfig.json resolve.alias = { "@": "/src" } // vite.config.tsБез
pathsCLI молча создаёт папку с именем@.Токены подключены один раз на приложение
Они приезжают вместе с записью — достаточно импортировать их в точке входа:
import "@/styles/motiva-tokens.css";Без них блок потеряет цвета, ритм и тему.
Своя типографика не спорит с блоком
Наследуемые свойства протекают внутрь:
text-align: centerна обёртке отцентрует содержимое секции. То же касаетсяfont-familyиline-height.Проект без TypeScript? всё равно нужен tsconfig
CLI грузит
tsconfig.jsonбезусловно, а при"tsx": falseидёт заjsconfig.jsonи падает так же. Минимум — файл сpathsи"tsx": trueвcomponents.json.Приехавший рядом
.tsxможно не трогать — вам нужен.vanilla.js.
Экспорт
Код
.gt::before, .gt::after {
content: attr(data-text);
position: absolute; inset: 0;
opacity: 0;
animation-duration: var(--gt-period);
animation-iteration-count: infinite;
/* прыжки, а не переходы: глитч дискретен по природе */
animation-timing-function: step-end;
}
.gt::before { color: oklch(0.65 0.25 25); animation-name: gt-a; }
.gt::after { color: oklch(0.75 0.15 195); animation-name: gt-b; }
/* два всплеска на цикл; между ними слои скрыты целиком */
@keyframes gt-a {
0%, 34% { opacity: 0; }
35% { opacity: 0.85; clip-path: inset(8% -2% 62% -2%); transform: translate(-3px, -1px); }
38% { clip-path: inset(48% -2% 22% -2%); transform: translate(3px, 1px); }
41% { clip-path: inset(72% -2% 4% -2%); transform: translate(-2px, 0); }
44%, 79% { opacity: 0; }
80% { opacity: 0.85; clip-path: inset(28% -2% 52% -2%); transform: translate(2px, 1px); }
83% { clip-path: inset(60% -2% 12% -2%); transform: translate(-3px, -1px); }
86%, 100% { opacity: 0; }
}
@keyframes gt-b {
0%, 35% { opacity: 0; }
36% { opacity: 0.85; clip-path: inset(55% -2% 18% -2%); transform: translate(3px, 1px); }
39% { clip-path: inset(12% -2% 70% -2%); transform: translate(-3px, -1px); }
42% { clip-path: inset(38% -2% 40% -2%); transform: translate(2px, 0); }
45%, 80% { opacity: 0; }
81% { opacity: 0.85; clip-path: inset(66% -2% 8% -2%); transform: translate(-2px, 1px); }
84% { clip-path: inset(18% -2% 58% -2%); transform: translate(3px, -1px); }
87%, 100% { opacity: 0; }
}
@media (prefers-reduced-motion: reduce) { .gt::before, .gt::after { display: none; } }
const css = `.gt::before, .gt::after {
content: attr(data-text);
position: absolute; inset: 0;
opacity: 0;
animation-duration: var(--gt-period);
animation-iteration-count: infinite;
/* прыжки, а не переходы: глитч дискретен по природе */
animation-timing-function: step-end;
}
.gt::before { color: oklch(0.65 0.25 25); animation-name: gt-a; }
.gt::after { color: oklch(0.75 0.15 195); animation-name: gt-b; }
/* два всплеска на цикл; между ними слои скрыты целиком */
@keyframes gt-a {
0%, 34% { opacity: 0; }
35% { opacity: 0.85; clip-path: inset(8% -2% 62% -2%); transform: translate(-3px, -1px); }
38% { clip-path: inset(48% -2% 22% -2%); transform: translate(3px, 1px); }
41% { clip-path: inset(72% -2% 4% -2%); transform: translate(-2px, 0); }
44%, 79% { opacity: 0; }
80% { opacity: 0.85; clip-path: inset(28% -2% 52% -2%); transform: translate(2px, 1px); }
83% { clip-path: inset(60% -2% 12% -2%); transform: translate(-3px, -1px); }
86%, 100% { opacity: 0; }
}
@keyframes gt-b {
0%, 35% { opacity: 0; }
36% { opacity: 0.85; clip-path: inset(55% -2% 18% -2%); transform: translate(3px, 1px); }
39% { clip-path: inset(12% -2% 70% -2%); transform: translate(-3px, -1px); }
42% { clip-path: inset(38% -2% 40% -2%); transform: translate(2px, 0); }
45%, 80% { opacity: 0; }
81% { opacity: 0.85; clip-path: inset(66% -2% 8% -2%); transform: translate(-2px, 1px); }
84% { clip-path: inset(18% -2% 58% -2%); transform: translate(3px, -1px); }
87%, 100% { opacity: 0; }
}
@media (prefers-reduced-motion: reduce) { .gt::before, .gt::after { display: none; } }
`;
import type { CSSProperties } from "react";
export interface GlitchTextProps {
/** Текст надписи (он же уходит в data-атрибут для слоёв-каналов). */
text?: string;
/** Секунд на цикл (в цикле два коротких всплеска). */
period?: number;
/** Цвет базовой надписи. */
color?: string;
}
/**
* Глитч-надпись: два слоя-канала (::before/::after с content: attr(data-text))
* сдвинуты в стороны и порезаны полосами clip-path. Весь «шум» — окна в процентах
* ОДНОГО цикла с step-end: значения прыгают, а не плывут, — глитч дискретен по природе.
* Вне всплесков слои полностью скрыты, видна чистая надпись.
* Reduced-motion / data-shot — слои спрятаны, остаётся обычный текст.
*/
export function GlitchText({ text = "ПОМЕХИ В КАНАЛЕ", period = 3.2, color = "oklch(0.93 0.01 265)" }: GlitchTextProps = {}) {
return (
<span
className="gt"
data-text={text}
style={
{
position: "relative",
display: "inline-block",
color,
fontWeight: 800,
letterSpacing: "0.02em",
whiteSpace: "nowrap",
"--gt-period": `${period}s`,
} as CSSProperties
}
>
<style href="motiva-glitch-text" precedence="medium">{css}</style>
{text}
</span>
);
}