Motiva
Мои коллекции
← каталог

Highlight Lines

Маркер ведёт по тексту вслед за прокруткой: строки подсвечиваются по очереди, как будто читают с фломастером. Ноль скриптов: у всех строк один view()-таймлайн, но каждая получает свой отрезок пути через animation-range инлайн-переменными. Маркер — фон строки, а не заливка глифов: этим отличается от stroke-fill-scroll.

cssscrollrevealintermediatereduced-motion ✓
↗
↕ демо длиннее кадра — прокрутите его

Пропсы

Разобрано из кода записи: обязательные — без «?», у остальных показано значение по умолчанию.

ПропТипПо умолчаниюЧто это
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
Что нужно в проекте до установки
  1. Файл components.json в корне

    Из него CLI узнаёт, куда класть файлы. Алиасы — в такой форме:

    "aliases": { "components": "@/components", "lib": "@/lib" }

    Нет файла — установка не начнётся вовсе.

  2. Алиас @ объявлен дважды

    В корневом tsconfig.json и в конфиге сборщика. В Vite корневой tsconfig часто содержит только references — этого мало.

    "paths": { "@/*": ["./src/*"] }        // tsconfig.json
    resolve.alias = { "@": "/src" }        // vite.config.ts

    Без paths CLI молча создаёт папку с именем @.

  3. Токены подключены один раз на приложение

    Они приезжают вместе с записью — достаточно импортировать их в точке входа:

    import "@/styles/motiva-tokens.css";

    Без них блок потеряет цвета, ритм и тему.

  4. Своя типографика не спорит с блоком

    Наследуемые свойства протекают внутрь: text-align: center на обёртке отцентрует содержимое секции. То же касается font-familyи line-height.

  5. Проект без TypeScript? всё равно нужен tsconfig

    CLI грузит tsconfig.json безусловно, а при "tsx": false идёт за jsconfig.json и падает так же. Минимум — файл с pathsи "tsx": true в components.json.

    Приехавший рядом .tsx можно не трогать — вам нужен .vanilla.js.

Экспорт

↓ Скачать .zip

Код

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>
  );
}
лицензия: MITавтор: Motivaисточник: оригинал (Motiva); идея-референс — Codrops «Some On-Scroll Text Highlight Animations» (код не заимствован)v1.0.0