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

Hero Centered

Центрированный первый экран: заголовок выезжает по словам, за текстом дышит аура акцента. Маска на уровне слова, поэтому переносы строк не ломаются.

cssloadrevealstaggerbasic~2 КБreduced-motion ✓

Редактируемые поля

Секция «hero». Весь контент приходит пропсами — ниже то, что конструктор даст поправить, не трогая код.

ПропЧто этоТип
badgeПлашка сверхуtext
titleЗаголовокtext
accentWordsСлов под акцентомtext
subПодзаголовокrichtext
primaryГлавная кнопкаtext
secondaryВторая кнопкаtext
noteСтрока под кнопкамиtext

Все пропсы

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

ПропТипПо умолчаниюЧто это
badge?string"бета открыта"Плашка над заголовком (маленькая строка в пилюле).
title?string"Соберите страницу за вечер"Заголовок целиком — слова выезжают по очереди.
accentWords?number2Последние N слов красятся акцентом (формат «2»).
sub?string"Готовые секции на одной дизайн-системе.…
primary?string"Начать бесплатно"
primaryHref?string"#"
secondary?string"Смотреть примеры"
secondaryHref?string"#"
note?string"без карты · открытый код · MIT"Мелкая строка под кнопками.
background?ReactNodeСокет фона: любой эффект с `mount.role: "background"`, например `<AuroraField />`. Занят — секция снимает собственную заливку, чтобы не спорить за слой.

Установка

npx shadcn@latest add https://motiva.pages.dev/r/hero-centered.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 { ReactNode } from "react";
export interface HeroCenteredProps {
  /** Плашка над заголовком (маленькая строка в пилюле). */
  badge?: string;
  /** Заголовок целиком — слова выезжают по очереди. */
  title?: string;
  /** Последние N слов красятся акцентом (формат «2»). */
  accentWords?: number;
  sub?: string;
  primary?: string;
  primaryHref?: string;
  secondary?: string;
  secondaryHref?: string;
  /** Мелкая строка под кнопками. */
  note?: string;
  /**
   * Сокет фона: любой эффект с `mount.role: "background"`, например `<AuroraField />`.
   * Занят — секция снимает собственную заливку, чтобы не спорить за слой.
   */
  background?: ReactNode;
  /** Какой текст читается поверх фона; приходит из `mount.tone` подставленной записи. */
  tone?: "dark" | "light";
  /** Плотность скрима между фоном и текстом (формат «0.5»); 0 — выключить. */
  scrim?: number;
}

/**
 * Центрированный герой — второй характер первого экрана: симметричный, «продуктовый»,
 * в противовес левому выключенному `hero-split`.
 *
 * Заголовок разбирается на слова прямо в разметке, и каждое получает свой порядковый
 * номер в переменной `--i`. Правило на появление при этом ОДНО: задержка считается из
 * номера. Добавил слово — оно встало в очередь само, ни JS, ни правки CSS.
 *
 * Разрывы строк при этом не ломаются: слова остаются инлайновыми, а «выезд из-под маски»
 * даёт вложенный `span` с `overflow: clip` — маска на уровне слова, а не строки.
 *
 * Аура за текстом дышит только `transform` и `opacity` — то есть целиком в композиторе.
 * Тот же вид через `background-position` или `filter` стоил бы перерисовки полэкрана
 * в каждом кадре.
 */
export function HeroCentered({
  badge = "бета открыта",
  title = "Соберите страницу за вечер",
  accentWords = 2,
  sub = "Готовые секции на одной дизайн-системе. Меняете текст и один цвет — получаете свою страницу, а не чужой шаблон.",
  primary = "Начать бесплатно",
  primaryHref = "#",
  secondary = "Смотреть примеры",
  secondaryHref = "#",
  note = "без карты · открытый код · MIT",
  background,
  tone,
  scrim = 0.5,
}: HeroCenteredProps) {
  const words = title.split(" ");
  const from = Math.max(0, words.length - accentWords);

  return (
    <section
      className="hce"
      data-slot-bg={background ? "" : undefined}
      data-tone={tone}
      style={{ ["--hce-scrim" as string]: scrim }}
    >
      <style>{`
        .hce { position: relative; width: 100%; overflow: hidden; text-align: center;
          padding-block: var(--ds-section-y, var(--ds-9, 120px));
          background: var(--ds-bg, oklch(0.14 0.02 275));
          color: var(--ds-fg, oklch(0.95 0.01 265));
          font-family: var(--ds-font, system-ui, sans-serif); }
        /* аура: transform + opacity, ни одной перерисовки */
        .hce::before { content: ""; position: absolute; left: 50%; top: 30%;
          width: min(900px, 110vw); aspect-ratio: 1; translate: -50% -50%; pointer-events: none;
          border-radius: 50%;
          background: radial-gradient(circle,
            var(--ds-accent-soft, oklch(0.72 0.16 300 / 0.16)), transparent 60%);
          animation: hce-breathe 11s ease-in-out infinite; }

        .hce-in { position: relative; max-width: min(var(--ds-container, 1120px), 880px);
          margin-inline: auto; padding-inline: var(--ds-gutter, clamp(18px, 5vw, 54px)); }

        .hce-badge { display: inline-flex; align-items: center; gap: var(--ds-2, 8px);
          margin: 0 0 var(--ds-5, 24px); padding: 5px 14px 5px 10px;
          border-radius: var(--ds-radius-pill, 999px);
          border: 1px solid var(--ds-border, oklch(0.3 0.02 275));
          background: var(--ds-surface, oklch(0.18 0.02 275));
          font-family: var(--ds-font-mono, ui-monospace, monospace);
          font-size: var(--ds-text-xs, 0.72rem); color: var(--ds-muted, oklch(0.72 0.02 265));
          animation: hce-fade 0.6s var(--ds-ease-out, cubic-bezier(0.16, 1, 0.3, 1)) both; }
        .hce-dot { width: 6px; height: 6px; border-radius: 50%;
          background: var(--ds-accent, oklch(0.72 0.16 300)); }

        .hce-h { margin: 0; font-size: var(--ds-display, clamp(2.2rem, 1rem + 7vw, 5.4rem));
          line-height: var(--ds-leading-tight, 0.98); font-weight: 800;
          letter-spacing: var(--ds-tracking-display, -0.045em); }
        /* маска на уровне СЛОВА: строки переносятся как обычно, выезд не ломает вёрстку */
        .hce-w { display: inline-block; overflow: clip; vertical-align: bottom; }
        .hce-w > span { display: inline-block;
          animation: hce-rise 0.85s calc(var(--i) * 0.045s)
            var(--ds-ease-out, cubic-bezier(0.16, 1, 0.3, 1)) both; }
        .hce-accent { background: linear-gradient(96deg,
            var(--ds-accent, oklch(0.78 0.16 300)), var(--ds-accent-2, oklch(0.76 0.14 220)));
          -webkit-background-clip: text; background-clip: text; color: transparent; }

        .hce-sub { margin: var(--ds-5, 24px) auto 0; max-width: 52ch;
          font-size: var(--ds-text-md, 1.02rem); line-height: var(--ds-leading-body, 1.55);
          color: var(--ds-muted, oklch(0.72 0.02 265));
          animation: hce-fade 0.6s 0.3s var(--ds-ease-out, cubic-bezier(0.16, 1, 0.3, 1)) both; }

        .hce-row { display: flex; flex-wrap: wrap; justify-content: center; gap: var(--ds-3, 12px);
          margin-top: var(--ds-6, 36px);
          animation: hce-fade 0.6s 0.42s var(--ds-ease-out, cubic-bezier(0.16, 1, 0.3, 1)) both; }
        .hce-btn { display: inline-block; text-decoration: none; cursor: pointer; border: 0;
          border-radius: var(--ds-radius-pill, 999px); padding: 14px 28px;
          font: 650 var(--ds-text-sm, 0.9rem) / 1 var(--ds-font, system-ui, sans-serif);
          transition: transform var(--ds-fast, 0.18s) var(--ds-ease-spring, cubic-bezier(0.34, 1.56, 0.64, 1)),
                      background var(--ds-fast, 0.18s); }
        .hce-btn:hover { transform: translateY(-2px); }
        .hce-btn:focus-visible { outline: 2px solid var(--ds-accent-ring, oklch(0.72 0.16 300 / 0.35));
          outline-offset: 3px; }
        .hce-primary { color: var(--ds-accent-fg, oklch(0.14 0.02 275));
          background: var(--ds-accent, oklch(0.72 0.16 300));
          box-shadow: var(--ds-shadow, 0 10px 34px oklch(0 0 0 / 0.45)); }
        .hce-primary:hover { background: var(--ds-accent-hover, oklch(0.78 0.16 300)); }
        .hce-ghost { color: var(--ds-fg, oklch(0.95 0.01 265)); background: transparent;
          box-shadow: inset 0 0 0 1px var(--ds-border, oklch(0.3 0.02 275)); }
        .hce-ghost:hover { background: var(--ds-surface, oklch(0.18 0.02 275)); }

        .hce-note { margin: var(--ds-4, 16px) 0 0;
          font-family: var(--ds-font-mono, ui-monospace, monospace);
          font-size: var(--ds-text-xs, 0.72rem); color: var(--ds-muted, oklch(0.72 0.02 265));
          opacity: 0.75;
          animation: hce-fade 0.6s 0.52s var(--ds-ease-out, cubic-bezier(0.16, 1, 0.3, 1)) both; }

        @keyframes hce-rise { from { transform: translateY(110%); } }
        @keyframes hce-fade { from { opacity: 0; transform: translateY(14px); } }
        @keyframes hce-breathe {
          0%, 100% { transform: scale(0.92); opacity: 0.8; }
          50% { transform: scale(1.08); opacity: 1; }
        }

        @media (prefers-reduced-motion: reduce) {
          .hce::before, .hce-badge, .hce-w > span, .hce-sub, .hce-row, .hce-note { animation: none; }
          .hce-btn { transition: none; }
        }
        html[data-shot] .hce::before, html[data-shot] .hce-badge, html[data-shot] .hce-w > span,
        html[data-shot] .hce-sub, html[data-shot] .hce-row, html[data-shot] .hce-note { animation: none; }
      
        /* СОКЕТ ФОНА. Слой лежит на z-index -1 внутри своего stacking context: он рисуется
           поверх заливки секции, но под всем содержимым — поэтому контенту не нужно ни
           position, ни z-index, и вёрстка секции не меняется вовсе.
           Тон переопределяет ТОКЕНЫ, а не перечисляет классы: все дети и так читают
           --ds-fg/--ds-muted, значит пара фон+текст чинится в одном месте.
           Скрим обязателен: у живого фона яркость гуляет по кадру, и цвет текста, читаемый
           и на провале, и на вспышке, подобрать нельзя — контраст надо создавать. */
        .hce { position: relative; isolation: isolate; }
        .hce-bg { position: absolute; inset: 0; z-index: -1; }
        .hce[data-slot-bg] { background: transparent; }
        .hce-bg::after { content: ""; position: absolute; inset: 0; pointer-events: none; }
        .hce[data-tone="dark"] { --ds-fg: oklch(0.96 0.01 265); --ds-muted: oklch(0.84 0.02 265);
          --ds-border: oklch(0.98 0.01 265 / 0.16); color: var(--ds-fg); }
        .hce[data-tone="dark"] .hce-bg::after {
          background: oklch(0.09 0.02 265 / calc(var(--hce-scrim, 0.5) * 0.9)); }
        .hce[data-tone="light"] { --ds-fg: oklch(0.2 0.02 265); --ds-muted: oklch(0.42 0.02 265);
          --ds-border: oklch(0.2 0.02 265 / 0.14); color: var(--ds-fg); }
        .hce[data-tone="light"] .hce-bg::after {
          background: oklch(0.99 0 0 / calc(var(--hce-scrim, 0.5) * 0.9)); }
      `}</style>

      {background && <div className="hce-bg">{background}</div>}

      <div className="hce-in">
        {badge && (
          <p className="hce-badge">
            <span className="hce-dot" aria-hidden />
            {badge}
          </p>
        )}

        <h1 className="hce-h">
          {words.map((w, i) => (
            <span key={`${w}-${i}`} className="hce-w" style={{ ["--i" as string]: i }}>
              <span className={i >= from ? "hce-accent" : undefined}>{w}</span>
              {i < words.length - 1 ? " " : ""}
            </span>
          ))}
        </h1>

        {sub && <p className="hce-sub">{sub}</p>}

        <div className="hce-row">
          {primary && (
            <a className="hce-btn hce-primary" href={primaryHref}>
              {primary}
            </a>
          )}
          {secondary && (
            <a className="hce-btn hce-ghost" href={secondaryHref}>
              {secondary}
            </a>
          )}
        </div>

        {note && <p className="hce-note">{note}</p>}
      </div>
    </section>
  );
}
лицензия: MITавтор: Motivaисточник: оригинал (Motiva)v1.0.0