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

Hero Video

Hero с фоновым видео — четвёртый вариант вида hero: несёт свой фон (сокета нет намеренно). Экономия в три яруса: постер отдельной картинкой под видео, ролик грузится и играет только когда секция видна, под reduced-motion видео не запрашивается вовсе.

cssloadrevealbasic~3 КБreduced-motion ✓
↗

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

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

ПропЧто этоТип
badgeПлашкаtext
titleЗаголовокtext
subПодзаголовокtext
primaryГлавная кнопкаtext
secondaryВторая кнопкаtext
noteМелкая строкаtext

Все пропсы

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

ПропТипПо умолчаниюЧто это
badge?string"экспедиции выходного дня"
title?string"Уехать туда, где не ловит"
sub?string"Собираем маршруты на два дня без толп и…
primary?string"Выбрать маршрут"
primaryHref?string"#"
secondary?string"Как это устроено"
secondaryHref?string"#"
note?string"ближайший выезд — в субботу · осталось …Мелкая строка под кнопками.

Установка

npx shadcn@latest add https://motiva.pages.dev/r/hero-video.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

Код

"use client";

import { useEffect, useRef, useState } from "react";
const css = `.hrv { position: relative; isolation: isolate; width: 100%;
  min-height: min(680px, 92svh);
  display: grid; align-content: center; justify-items: center;
  padding-block: var(--ds-section-y, var(--ds-8, 80px));
  overflow: clip;
  color: oklch(0.97 0.005 265);
  font-family: var(--ds-font, system-ui, sans-serif);
  text-align: center; }

/* видео — СВОЙ фон секции (сокета нет намеренно: два фона спорили бы за слой).
   Постер лежит отдельной картинкой ПОД видео: пока ролик не готов — и там,
   где он не грузится вовсе (reduced-motion, экономия) — кадр уже стоит */
.hrv-media { position: absolute; inset: 0; z-index: -1; }
.hrv-media img,
.hrv-media video { position: absolute; inset: 0; width: 100%; height: 100%;
  object-fit: cover; }
.hrv-media::after { content: ""; position: absolute; inset: 0; pointer-events: none;
  background:
    linear-gradient(oklch(0.08 0.02 265 / calc(var(--hrv-scrim, 0.55) * 0.7)),
      oklch(0.08 0.02 265 / var(--hrv-scrim, 0.55))); }

.hrv-in { max-width: min(46rem, 92%); padding-inline: var(--ds-gutter, clamp(18px, 5vw, 54px)); }

.hrv-badge { display: inline-block; margin: 0 0 var(--ds-4, 16px); padding: 6px 14px;
  border-radius: 999px; border: 1px solid oklch(0.98 0.01 265 / 0.28);
  background: oklch(0.1 0.02 265 / 0.35); backdrop-filter: blur(6px);
  font-family: var(--ds-font-mono, ui-monospace, monospace);
  font-size: var(--ds-text-xs, 0.75rem); letter-spacing: 0.14em; text-transform: uppercase; }

.hrv-h { margin: 0; font-size: clamp(2.2rem, 6vw, 4rem); font-weight: 800;
  line-height: 1.04; letter-spacing: var(--ds-tracking-tight, -0.03em);
  text-wrap: balance;
  text-shadow: 0 2px 24px oklch(0.05 0.02 265 / 0.45); }

.hrv-sub { margin: var(--ds-4, 16px) auto 0; max-width: 52ch;
  font-size: var(--ds-text-md, 1.05rem); line-height: var(--ds-leading-body, 1.55);
  color: oklch(0.93 0.01 265 / 0.92); }

.hrv-row { display: flex; gap: var(--ds-3, 12px); justify-content: center; flex-wrap: wrap;
  margin-top: var(--ds-6, 32px); }
.hrv-btn { padding: 13px 26px; border-radius: 999px; cursor: pointer;
  font: 600 var(--ds-text-sm, 0.95rem) / 1 var(--ds-font, system-ui, sans-serif);
  text-decoration: none; border: 1px solid transparent;
  transition: filter 0.2s ease, background-color 0.2s ease; }
.hrv-btn:hover { filter: brightness(1.1); }
.hrv-btn:focus-visible { outline: 2px solid oklch(0.98 0.01 265 / 0.6); outline-offset: 2px; }
.hrv-primary { background: var(--ds-accent, oklch(0.72 0.16 300));
  color: var(--ds-accent-fg, oklch(0.14 0.02 275)); }
.hrv-secondary { background: oklch(0.1 0.02 265 / 0.35); color: inherit;
  border-color: oklch(0.98 0.01 265 / 0.3); backdrop-filter: blur(6px); }

.hrv-note { margin: var(--ds-4, 18px) 0 0;
  font-family: var(--ds-font-mono, ui-monospace, monospace);
  font-size: var(--ds-text-xs, 0.75rem); color: oklch(0.9 0.01 265 / 0.75); }
`;

export interface HeroVideoProps {
  badge?: string;
  title?: string;
  sub?: string;
  primary?: string;
  primaryHref?: string;
  secondary?: string;
  secondaryHref?: string;
  /** Мелкая строка под кнопками. */
  note?: string;
  /** Фоновый ролик; путь вида `https://motiva.pages.dev/video/<имя>.mp4`. */
  src?: string;
  /** Кадр-заставка; стоит до готовности ролика и вместо него под reduced-motion. */
  poster?: string;
  /** Плотность затемнения над видео; 0 — выключить. */
  scrim?: number;
}

/**
 * HERO С ФОНОВЫМ ВИДЕО — четвёртый вариант вида hero: там, где centered/split
 * берут сокетный шейдер, этот несёт СВОЙ фон, поэтому сокета нет намеренно —
 * два фона спорили бы за слой.
 *
 * Экономия честная, в три яруса: постер — отдельная картинка ПОД видео (кадр
 * стоит мгновенно); сам ролик с `preload="none"` начинает грузиться и играть
 * только когда секция ВИДНА (IntersectionObserver — hero ниже первого экрана
 * не тратит ни байта); под reduced-motion и data-shot видео не запрашивается
 * ВОВСЕ — остаётся постер. Ролик muted+loop+playsInline, без звука и управления:
 * это фон, а не контент — для контента есть video-facade и video-chapters.
 */
export function HeroVideo({
  badge = "экспедиции выходного дня",
  title = "Уехать туда, где не ловит",
  sub = "Собираем маршруты на два дня без толп и связи: заброска, ночёвка, обратный поезд. Вы берёте рюкзак — остальное уже собрано.",
  primary = "Выбрать маршрут",
  primaryHref = "#",
  secondary = "Как это устроено",
  secondaryHref = "#",
  note = "ближайший выезд — в субботу · осталось 4 места",
  src = "https://motiva.pages.dev/video/smoke-1280x720.mp4",
  poster = "https://motiva.pages.dev/video/smoke-1280x720-poster.jpg",
  scrim = 0.55,
}: HeroVideoProps) {
  const host = useRef<HTMLElement>(null);
  const [play, setPlay] = useState(false);

  useEffect(() => {
    const el = host.current;
    if (!el) return;
    const frozen =
      document.documentElement.hasAttribute("data-shot") ||
      window.matchMedia("(prefers-reduced-motion: reduce)").matches;
    if (frozen) return; // видео не запрашивается вовсе — стоит постер
    const io = new IntersectionObserver(
      ([e]) => {
        if (e?.isIntersecting) {
          setPlay(true);
          io.disconnect();
        }
      },
      { rootMargin: "120px" },
    );
    io.observe(el);
    return () => io.disconnect();
  }, []);

  return (
    <section
      ref={host}
      className="hrv"
      style={{ ["--hrv-scrim" as string]: scrim }}
    >
      <style href="motiva-hero-video" precedence="medium">{css}</style>

      <div className="hrv-media" aria-hidden="true">
        <img src={poster} alt="" loading="eager" decoding="async" />
        {play && <video src={src} poster={poster} muted loop playsInline autoPlay preload="none" />}
      </div>

      <div className="hrv-in">
        {/* data-slot: адрес слота для inline-правки в конструкторе (/build) */}
        {badge && <p className="hrv-badge" data-slot="badge">{badge}</p>}
        {title && <h1 className="hrv-h" data-slot="title">{title}</h1>}
        {sub && <p className="hrv-sub" data-slot="sub">{sub}</p>}

        {(primary || secondary) && (
          <div className="hrv-row">
            {primary && (
              <a className="hrv-btn hrv-primary" href={primaryHref} data-slot="primary">{primary}</a>
            )}
            {secondary && (
              <a className="hrv-btn hrv-secondary" href={secondaryHref} data-slot="secondary">{secondary}</a>
            )}
          </div>
        )}

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