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

Tracing Beam

Луч вдоль контента, прорисовываемый прокруткой: pathLength=1 и stroke-dashoffset 1→0 от view-timeline. Ни строки JS, откат прокрутки отматывает луч.

cssscrollintermediate0reduced-motion ✓
↕ демо длиннее кадра — прокрутите его
🎛 Настройки — крути и копируй

Пропсы

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

ПропТипПо умолчаниюЧто это
hue?number300Тон луча, градусы oklch.
children?ReactNodeКонтент, вдоль которого рисуется луч.

Установка

npx shadcn@latest add https://motiva.pages.dev/r/tracing-beam.json
Что нужно в проекте до установки
  1. components.json в корне — с алиасами в форме"components": "@/components". Нет файла — CLI не поймёт, куда класть.
  2. Алиас @ объявлен дважды: "paths": {"@/*": ["./src/*"]} в корневом tsconfig.json (в Vite он часто содержит только references — тогда CLI создаёт папку@) и resolve.alias в конфиге сборщика.
  3. Токены подключены один раз на приложение:import "@/styles/motiva-tokens.css" — они приезжают вместе с записью. Без них секция потеряет цвета, ритм и тему.
  4. Своя типографика не должна спорить: если у вас на обёрткеtext-align: center, он протечёт внутрь блоков.
  5. Проект без TypeScript? Всё равно нужен минимальныйtsconfig.json с pathsи "tsx": true в components.json: CLI грузит tsconfig безусловно, а с "tsx": false идёт за jsconfig.json и падает так же. Приехавший рядом.tsx можно не трогать — вам нужен.vanilla.js.

Экспорт

↓ Скачать .zip

Код

import type { CSSProperties, ReactNode } from "react";

export interface TracingBeamProps {
  /** Тон луча, градусы oklch. */
  hue?: number;
  /** Контент, вдоль которого рисуется луч. */
  children?: ReactNode;
}

/**
 * Луч вдоль контента, прорисовываемый прокруткой: у линии pathLength = 1, поэтому
 * dash-паттерн задан долями, и прогресс — это просто stroke-dashoffset 1 → 0.
 * Прогресс отдаёт браузер (именованный view-timeline на внешней коробке — она и
 * проходит через экран, AGENTS), скрипта нет вовсе, откат прокрутки отматывает луч.
 * Без поддержки animation-timeline и в reduced-motion луч дорисован полностью.
 */
export function TracingBeam({ hue = 300, children }: TracingBeamProps = {}) {
  const bright = `oklch(0.72 0.14 ${hue})`;
  return (
    <div className="tb" style={{ display: "grid", gridTemplateColumns: "22px minmax(0, 1fr)", gap: 22, width: "100%" }}>
      <style>{`
        .tb { view-timeline: --tb-view block; }
        .tb-rail { position: relative; container-type: size; }
        /* база (вне gvarda supports): луч дорисован — так же выглядит reduced-motion */
        .tb-draw { stroke-dashoffset: 0; }
        .tb-dot {
          position: absolute; left: 50%; top: 4px;
          width: 12px; height: 12px; margin-left: -6px;
          border-radius: 999px;
          background: var(--tb-bright);
          box-shadow: 0 0 12px var(--tb-bright);
        }
        @supports (animation-timeline: view()) {
          /* contain, не cover: у колонки выше экрана это ровно «прогресс чтения» —
             0 когда её верх коснулся верха экрана, 1 когда низ дошёл до низа.
             cover требовал бы, чтобы коробка целиком ПОКИНУЛА экран, — на странице,
             которая кончается вместе с колонкой, луч не дорисовывался никогда. */
          .tb-draw {
            stroke-dashoffset: 1;
            animation: tb-draw linear both;
            animation-timeline: --tb-view;
            animation-range: contain;
          }
          .tb-dot {
            animation: tb-move linear both;
            animation-timeline: --tb-view;
            animation-range: contain;
          }
          @keyframes tb-draw { from { stroke-dashoffset: 1; } to { stroke-dashoffset: 0; } }
          @keyframes tb-move { from { transform: translateY(0); } to { transform: translateY(calc(100cqh - 16px)); } }
        }
        @media (prefers-reduced-motion: reduce) {
          .tb-draw { animation: none; stroke-dashoffset: 0; }
          .tb-dot { display: none; }
        }
      `}</style>
      <div className="tb-rail" style={{ "--tb-bright": bright } as CSSProperties}>
        <svg width="22" height="100%" style={{ position: "absolute", inset: 0, display: "block" }} aria-hidden="true">
          <line x1="11" y1="8" x2="11" y2="100%" stroke="oklch(0.32 0.02 270 / 0.8)" strokeWidth="2" />
          <line
            className="tb-draw"
            x1="11"
            y1="8"
            x2="11"
            y2="100%"
            pathLength={1}
            strokeDasharray="1"
            stroke={bright}
            strokeWidth="2"
            strokeLinecap="round"
          />
        </svg>
        <span className="tb-dot" />
      </div>
      <div style={{ minWidth: 0 }}>{children}</div>
    </div>
  );
}
лицензия: MITавтор: Motivaисточник: оригинал (Motiva); идея-референс — Aceternity Tracing Beam (только идея: terms запрещают заимствование кода)v1.0.0