Stroke Fill Scroll
Контурный заголовок заливается цветом по мере прохождения через кадр — индикатор прогресса, живущий прямо в буквах, ноль скриптов. Копия текста с градиентом, вырезанным по глифам (background-clip: text), растёт через background-size на animation-timeline: view() — и отматывается назад вместе с прокруткой.
Пропсы
Разобрано из кода записи: обязательные — без «?», у остальных показано значение по умолчанию.
| Проп | Тип | По умолчанию | Что это |
|---|---|---|---|
| text? | string | "Прогресс живёт в буквах" | Текст заголовка. |
| strokeWidth? | number | 1.2 | Толщина контура, px (формат «1.2»). |
| fontSize? | string | "clamp(2rem, 7cqw, 3.6rem)" | Кегль: любое CSS-значение font-size. |
Установка
npx shadcn@latest add https://motiva.pages.dev/r/stroke-fill-scroll.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.
Экспорт
Код
.sfsc {
position: relative;
margin: 0;
color: transparent;
-webkit-text-stroke: var(--sfsc-stroke) var(--ds-muted, oklch(0.52 0.02 265));
/* правка width/height не влияет на заливку: она идёт background-size у копии */
}
/* заливка — КОПИЯ текста поверх контура: градиент акцента, вырезанный по глифам.
База = залито целиком: без поддержки прокруточных анимаций, под reduce и data-shot
заголовок просто цветной */
.sfsc::before {
content: attr(data-text);
position: absolute;
inset: 0;
color: transparent;
-webkit-text-stroke: 0;
background-image: linear-gradient(
90deg,
var(--ds-accent, oklch(0.72 0.16 300)),
oklch(from var(--ds-accent, oklch(0.72 0.16 300)) calc(l + 0.12) c calc(h + 30))
);
background-repeat: no-repeat;
background-size: 100% 100%;
-webkit-background-clip: text;
background-clip: text;
}
@supports (animation-timeline: view()) {
/* начальное состояние ВНУТРИ @supports: там, где прокруточных анимаций нет,
заголовок обязан остаться залитым, а не пустым навсегда */
.sfsc::before {
background-size: 0% 100%;
animation: sfsc-fill linear both;
animation-timeline: view();
/* субъект мал, contain применим; заливка заканчивается к середине кадра —
читатель видит её завершённой, а не догоняет у кромки */
animation-range: contain 0% cover 55%;
}
@keyframes sfsc-fill {
to { background-size: 100% 100%; }
}
}
@media (prefers-reduced-motion: reduce) { .sfsc::before { animation: none; } }
html[data-shot] .sfsc::before { animation: none; }
const css = `.sfsc {
position: relative;
margin: 0;
color: transparent;
-webkit-text-stroke: var(--sfsc-stroke) var(--ds-muted, oklch(0.52 0.02 265));
/* правка width/height не влияет на заливку: она идёт background-size у копии */
}
/* заливка — КОПИЯ текста поверх контура: градиент акцента, вырезанный по глифам.
База = залито целиком: без поддержки прокруточных анимаций, под reduce и data-shot
заголовок просто цветной */
.sfsc::before {
content: attr(data-text);
position: absolute;
inset: 0;
color: transparent;
-webkit-text-stroke: 0;
background-image: linear-gradient(
90deg,
var(--ds-accent, oklch(0.72 0.16 300)),
oklch(from var(--ds-accent, oklch(0.72 0.16 300)) calc(l + 0.12) c calc(h + 30))
);
background-repeat: no-repeat;
background-size: 100% 100%;
-webkit-background-clip: text;
background-clip: text;
}
@supports (animation-timeline: view()) {
/* начальное состояние ВНУТРИ @supports: там, где прокруточных анимаций нет,
заголовок обязан остаться залитым, а не пустым навсегда */
.sfsc::before {
background-size: 0% 100%;
animation: sfsc-fill linear both;
animation-timeline: view();
/* субъект мал, contain применим; заливка заканчивается к середине кадра —
читатель видит её завершённой, а не догоняет у кромки */
animation-range: contain 0% cover 55%;
}
@keyframes sfsc-fill {
to { background-size: 100% 100%; }
}
}
@media (prefers-reduced-motion: reduce) { .sfsc::before { animation: none; } }
html[data-shot] .sfsc::before { animation: none; }
`;
import type { CSSProperties } from "react";
export interface StrokeFillScrollProps {
/** Текст заголовка. */
text?: string;
/** Толщина контура, px (формат «1.2»). */
strokeWidth?: number;
/** Кегль: любое CSS-значение font-size. */
fontSize?: string;
}
/**
* Контурный заголовок заливается цветом по мере прохождения через кадр — как
* индикатор прогресса, живущий прямо в буквах. Ноль скриптов.
*
* Устройство: под контуром лежит КОПИЯ текста (`::before` + `attr(data-text)`),
* у копии градиент акцента вырезан по глифам (`background-clip: text`), а растёт
* он через `background-size: 0% → 100%` на `animation-timeline: view()` — то есть
* заливка привязана к положению заголовка в кадре и отматывается назад вместе
* с прокруткой. Начальное состояние (0%) объявлено ВНУТРИ `@supports`: там, где
* прокруточных анимаций нет, как и под `prefers-reduced-motion` и `data-shot`,
* заголовок стоит залитым целиком.
*
* Цвет — пара токенов дизайн-системы (`--ds-accent` + производная через
* `oklch(from …)`), контур — `--ds-muted`: запись перекрашивается темой.
*/
export function StrokeFillScroll({
text = "Прогресс живёт в буквах",
strokeWidth = 1.2,
fontSize = "clamp(2rem, 7cqw, 3.6rem)",
}: StrokeFillScrollProps = {}) {
return (
<h2
className="sfsc"
data-text={text}
style={
{
font: `700 ${fontSize}/1.12 system-ui, sans-serif`,
letterSpacing: "-0.02em",
"--sfsc-stroke": `${strokeWidth}px`,
} as CSSProperties
}
>
<style href="motiva-stroke-fill-scroll" precedence="medium">{css}</style>
{text}
</h2>
);
}