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

Clip Reveal

Блок проявляется через раскрывающийся вырез, пока входит в кадр: снизу вверх, из середины, шторкой или по диагонали. Прогресс ведёт прокруточный таймлайн — ни скрипта, ни наблюдателя, в покое не работает ничего.

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

Пропсы

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

ПропТипПо умолчаниюЧто это
items?ClipRevealItem[]DEFAULT_ITEMS
ClipRevealItem — что внутри
  • title: string
  • text: string
  • img?: string
  • shape?: ClipRevealShape— Форма выреза: снизу вверх · из середины · шторкой от краёв · по диагонали.

Установка

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

Код

/* Появление через клип: форма выреза раскрывается, пока блок входит в кадр.

   Прогресс ведёт САМ БРАУЗЕР по прокрутке (`animation-timeline: view()`), а не скрипт.
   Разница не вкусовая: скриптовый вариант держит цикл кадров живым и в покое, а тут
   в покое не работает ничего — таймлайн у анимации не временной, а прокруточный.

   Порядок объявлений — главная грабля записи. ОТКРЫТОЕ состояние стоит СНАРУЖИ
   `@supports`, закрытое — только внутри. Наоборот было бы нельзя: там, где
   прокруточных таймлайнов нет (и при `prefers-reduced-motion`), содержимое осталось бы
   закрытым НАВСЕГДА — эффект превратился бы в «текста нет». */

.clr {
  width: 100%;
  font-family: var(--ds-font, system-ui, -apple-system, sans-serif);
  color: var(--ds-fg, oklch(0.92 0.01 265));
}

.clr-list {
  display: grid;
  gap: var(--ds-6, 32px);
  max-width: 640px;
  margin: 0 auto;
  padding: 0;
  list-style: none;
}

/* Безопасное умолчание: открыто. */
.clr-item {
  position: relative;
  clip-path: inset(0 0 0 0);
  border-radius: var(--ds-radius, 14px);
  overflow: hidden;
  background: var(--ds-surface, oklch(0.17 0.02 270));
  border: 1px solid var(--ds-border, oklch(0.3 0.02 270));
}

.clr-img {
  display: block;
  width: 100%;
  height: 190px;
  object-fit: cover;
}

.clr-body {
  display: grid;
  gap: 4px;
  padding: 14px 16px 16px;
}

.clr-title {
  font-size: var(--ds-text-md, 1rem);
  font-weight: 640;
  letter-spacing: var(--ds-tracking-tight, -0.02em);
}

.clr-text {
  margin: 0;
  font-size: var(--ds-text-sm, 0.85rem);
  line-height: 1.5;
  color: var(--ds-muted, oklch(0.7 0.02 265));
}

.clr-shape {
  font-family: var(--ds-font-mono, ui-monospace, Menlo, monospace);
  font-size: 0.72rem;
  color: var(--ds-accent, oklch(0.72 0.16 300));
}

@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) {
    .clr-item {
      /* субъект таймлайна — сам блок: «его собственное прохождение через кадр».
         Именно view(), а не scroll(): нужен прогресс ЭТОГО блока, а не страницы. */
      view-timeline-name: --clr-view;
      view-timeline-axis: block;
      animation: clr-up linear both;
      animation-timeline: --clr-view;
      /* Диапазон меряется по COVER, а не по ENTRY: entry заканчивается, когда блок
         ЦЕЛИКОМ вошёл в кадр, то есть у нижней кромки — раскрытие успевало отработать
         внизу экрана и до центра доезжало уже открытым. cover 48% — это момент, когда
         центр блока совпадает с центром кадра: приём срабатывает там, куда смотрят. */
      animation-range: var(--clr-from, cover 18%) var(--clr-to, cover 48%);
    }

    .clr-item[data-shape="center"] {
      animation-name: clr-center;
    }

    .clr-item[data-shape="curtain"] {
      animation-name: clr-curtain;
    }

    .clr-item[data-shape="diagonal"] {
      animation-name: clr-diagonal;
    }
  }
}

/* В каждой паре кадров ОДНА И ТА ЖЕ функция и одинаковое число точек — иначе
   интерполировать нечего и переход выглядит как рывок в конце. */
@keyframes clr-up {
  from {
    clip-path: inset(100% 0 0 0);
  }
  to {
    clip-path: inset(0 0 0 0);
  }
}

@keyframes clr-center {
  from {
    clip-path: inset(50% 0 50% 0);
  }
  to {
    clip-path: inset(0 0 0 0);
  }
}

@keyframes clr-curtain {
  from {
    clip-path: inset(0 50% 0 50%);
  }
  to {
    clip-path: inset(0 0 0 0);
  }
}

@keyframes clr-diagonal {
  from {
    clip-path: polygon(0% 0%, 0% 0%, 0% 100%, 0% 100%);
  }
  to {
    clip-path: polygon(0% 0%, 140% 0%, 100% 100%, 0% 100%);
  }
}

html[data-shot] .clr-item {
  animation: none;
  clip-path: inset(0 0 0 0);
}
лицензия: MITавтор: Motivaисточник: оригинал (Motiva)v1.0.0