Highlight Lines
Маркер ведёт по тексту вслед за прокруткой: строки подсвечиваются по очереди, как будто читают с фломастером. Ноль скриптов: у всех строк один view()-таймлайн, но каждая получает свой отрезок пути через animation-range инлайн-переменными. Маркер — фон строки, а не заливка глифов: этим отличается от stroke-fill-scroll.
Пропсы
Разобрано из кода записи: обязательные — без «?», у остальных показано значение по умолчанию.
| Проп | Тип | По умолчанию | Что это |
|---|---|---|---|
| lines? | string[] | LINES | Строки: автор делит текст сам — перенос и есть смысловая пауза маркера. |
| fontSize? | string | "clamp(1.3rem, 3.6cqw, 2.1rem)" | Кегль: любое CSS-значение font-size. |
Установка
npx shadcn@latest add https://motiva.pages.dev/r/highlight-lines.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.
Экспорт
Код
import type { CSSProperties } from "react";
const css = `.hll {
margin: 0;
display: grid;
gap: 0.18em;
font-weight: 700;
line-height: 1.22;
letter-spacing: var(--ds-tracking-tight, -0.03em);
color: var(--ds-fg, oklch(0.95 0.01 265));
}
/* маркер — фон строки, а не глифов: полупрозрачная плашка акцента ЗА текстом.
width: max-content — плашка заканчивается со словами, а не тянется до края.
База = выделено целиком: без поддержки, под reduce и data-shot текст просто
стоит промаркированным */
.hll-line {
width: max-content;
max-width: 100%;
padding-inline: 0.14em;
background-image: linear-gradient(
oklch(from var(--ds-accent, oklch(0.72 0.16 300)) l c h / 0.38),
oklch(from var(--ds-accent, oklch(0.72 0.16 300)) l c h / 0.38)
);
background-repeat: no-repeat;
background-size: 100% 78%;
background-position: 0 88%;
}
@supports (animation-timeline: view()) {
@media not (prefers-reduced-motion: reduce) {
/* последовательность даёт РАЗНЫЙ animation-range у строк (инлайн-переменными):
у всех один view()-таймлайн абзаца, но каждая строка занимает свой отрезок
пути — маркер честно идёт строка за строкой и отматывается назад */
.hll-line {
background-size: 0% 78%;
animation: hll-mark linear both;
animation-timeline: view();
animation-range: var(--hll-r0, cover 10%) var(--hll-r1, cover 60%);
}
@keyframes hll-mark {
to { background-size: 100% 78%; }
}
}
}
html[data-shot] .hll-line { animation: none; }
`;
export interface HighlightLinesProps {
/** Строки: автор делит текст сам — перенос и есть смысловая пауза маркера. */
lines?: string[];
/** Кегль: любое CSS-значение font-size. */
fontSize?: string;
}
const LINES = [
"Хороший интерфейс не просит внимания —",
"он выделяет главное сам,",
"ровно тогда, когда до него дочитали.",
];
/**
* Маркер ведёт по тексту вслед за прокруткой: строки подсвечиваются ПО ОЧЕРЕДИ,
* как будто кто-то читает с фломастером в руке. Ноль скриптов.
*
* Хитрость последовательности: у всех строк ОДИН таймлайн (`view()` абзаца),
* но каждая получает свой отрезок пути через `animation-range` инлайн-переменными —
* первая закрашивается раньше, последняя позже, и всё честно отматывается назад.
* Сам маркер — фон СТРОКИ, а не глифов (`background-size` по плашке за текстом),
* поэтому он выглядит фломастером, а не заливкой букв — этим запись и отличается
* от `stroke-fill-scroll`.
*
* База (без поддержки, `prefers-reduced-motion`, `data-shot`) — текст стоит
* промаркированным целиком.
*/
export function HighlightLines({
lines = LINES,
fontSize = "clamp(1.3rem, 3.6cqw, 2.1rem)",
}: HighlightLinesProps = {}) {
const n = lines.length;
const span = 52 / n;
return (
<p className="hll" style={{ fontSize } as CSSProperties}>
<style href="motiva-highlight-lines" precedence="medium">{css}</style>
{lines.map((line, i) => {
const start = 8 + i * span;
const end = start + span + 6;
return (
<span
key={line}
className="hll-line"
style={
{
"--hll-r0": `cover ${start.toFixed(1)}%`,
"--hll-r1": `cover ${end.toFixed(1)}%`,
} as CSSProperties
}
>
{line}
</span>
);
})}
</p>
);
}