Carousel
Anchor Deck
Карусель без единой строки JS, но со стрелками: пара стрелок лежит внутри каждого слайда и ведёт на соседний якорь, поэтому текущая позиция компоненту не нужна.
- carousel
- scroll-snap
- no-js
- anchors
Превью
Использовать с AI
- 1. Скопируйте ссылку.
- 2. Напишите агенту своими словами и вставьте её в предложение.
- 3. Агент откроет ссылку и поставит компонент из registry.
размести это в шапке: https://vibeui.ru/c/carousel-014
- ‹
Сначала данные
Компонент получает массив и рисует ровно то, что в нём есть
› - ‹
Потом раскладка
Ширина считается от контейнера, а не от окна браузера
› - ‹
И только затем цвет
Палитра живёт в локальных переменных и не спорит с вашей темой
› - ‹
Ничего лишнего
Ни рантайма, ни зависимостей: один файл в вашем проекте
›
Листается пальцем, колесом и стрелками внутри кадра — без JavaScript.
Показать полную инструкцию
То, что лежит по ссылке в развёрнутом виде. Нужна, если агент не может открыть ссылку — тогда вставьте этот текст целиком.
# Установка и размещение «carousel-014» (Anchor Deck) из VibeUI
## 1. Сначала установка — не пропускай, не пересоздавай
Выполни эту команду до того, как писать код:
npx shadcn@latest add https://vibeui.ru/r/carousel-014.json
Registry item: https://vibeui.ru/r/carousel-014.json
Ставится в: components/vibeui/carousel-014.tsx (точный путь следует алиасам components.json этого проекта).
npm-зависимости: нет.
Registry-зависимости: нет.
Ставь из реестра. Не пересоздавай по описанию, не подменяй похожим
компонентом из другой библиотеки и не переписывай под стиль проекта.
## 2. Что это
Карусель без единой строки JS, но со стрелками: пара стрелок лежит внутри каждого слайда и ведёт на соседний якорь, поэтому текущая позиция компоненту не нужна.
Карусель на scroll-snap без JS: стрелки внутри слайдов — якорные ссылки на соседей. Ноль зависимостей, один файл, клиентского JS нет.
## 3. Как использовать
import { Carousel014 } from "@/components/vibeui/carousel-014"
<Carousel014
label="Принципы"
idPrefix="principles"
cards={[
{ title: "Сначала данные", text: "Компонент рисует то, что получил", hue: 250 },
{ title: "Потом раскладка", text: "Ширина считается от контейнера", hue: 160 },
]}
/>
Полный список пропсов — в установленном файле.
## 4. Куда поставить
Это мелкий inline-компонент. Поставь его ровно туда, куда просил
пользователь, внутрь существующей разметки. Не создавай ради него
новую страницу, секцию или обёртку. Если на этом месте уже стоит похожий
контрол — замени его, а не добавляй второй.
Куда поставить: ___
(Эту строку заполняет пользователь. Если она осталась пустой — спроси,
а не угадывай.)
## 5. Сохранить как установлено
- локальную палитру --vibeui-carousel-014-* и не подменять её токенами вашей темы
- стрелки внутри слайдов: только так «предыдущий» и «следующий» известны без знания позиции
- scroll-snap-stop:always — иначе быстрый свайп проскакивает через два слайда
- идентификаторы из idPrefix: два блока на странице с одинаковыми якорями сломают переходы
- подписи стрелок с названием соседнего слайда: «вперёд» без цели ничего не сообщает
- нативную прокрутку: палец, колесо, Tab и клавиши работают без обработчиков
## 6. Можно менять
- слайды через cards: заголовок, текст и оттенок
- подпись карусели через label
- основу идентификаторов через idPrefix
- пропорции кадра в правиле aspect-ratio у [data-part="slide"]
## 7. Правила
- Переход по якорю добавляет запись в историю браузера: кнопка «назад» вернёт на прошлый слайд.
- Активный слайд компонент не знает, поэтому точек-индикаторов здесь нет и быть не может.
- Лента зациклена по ссылкам: последняя стрелка ведёт на первый слайд.
- Не выносите CSS-переменные в globals.css: компонент обязан оставаться одним файлом.
## 8. Проверка
- рендерится без ошибок в консоли;
- выглядит так же, как превью на странице VibeUI, откуда взята ссылка;
- если нет — ты изменил что-то из раздела 5, верни как было.Для разработчика
npx shadcn@latest add https://vibeui.ru/r/carousel-014.jsonhttps://vibeui.ru/r/carousel-014.jsonКомпонент самодостаточен: один файл, без зависимостей, палитра в локальных переменных --vibeui-carousel-014-*. Серверный: хуков нет вообще. Прокрутку и остановку держит scroll-snap-type:x mandatory со scroll-snap-stop:always, поэтому лента не проскакивает через слайд. Стрелки — обычные ссылки на идентификаторы соседних слайдов: браузер сам доезжает до нужного, а «предыдущий» и «следующий» известны на рендере. Идентификаторы собираются из пропа idPrefix, чтобы два блока на одной странице не перебили якоря друг друга.
Исходник компонента
Тот же файл, который поставит агент. Нужен, если вы предпочитаете скопировать код руками.
import type { ComponentPropsWithoutRef, CSSProperties } from "react"
export type Carousel014Card = {
title: string
text?: string
hue?: number
}
export type Carousel014Props = Omit<
ComponentPropsWithoutRef<"section">,
"children"
> & {
cards?: Carousel014Card[]
label?: string
/** Основа идентификаторов слайдов: два блока на одной странице не должны совпасть. */
idPrefix?: string
}
// Идея компонента: карусель без единой строки JS, но со стрелками. Пара
// стрелок лежит внутри каждого слайда и ведёт на соседний якорь — поэтому
// «предыдущий» и «следующий» известны заранее, без знания текущей позиции.
// Прокрутку и остановку держит scroll-snap: палец, колесо, Tab и клавиши
// работают сами, а компонент остаётся серверным.
const STYLES = `
:where([data-vibeui-block="carousel-014"]){
--vibeui-carousel-014-bg:oklch(1 0 0);
--vibeui-carousel-014-fg:oklch(0.22 0.014 265);
--vibeui-carousel-014-muted:oklch(0.58 0.014 265);
--vibeui-carousel-014-border:oklch(0.91 0.006 265);
--vibeui-carousel-014-accent:oklch(0.55 0.17 265);
--vibeui-carousel-014-font:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;
}
[data-vibeui-block="carousel-014"]{
display:flex;flex-direction:column;gap:0.5rem;
width:100%;max-width:26rem;box-sizing:border-box;padding:0.875rem;
background:var(--vibeui-carousel-014-bg);
border:1px solid var(--vibeui-carousel-014-border);border-radius:1rem;
font-family:var(--vibeui-carousel-014-font);color:var(--vibeui-carousel-014-fg);
}
[data-vibeui-block="carousel-014"] [data-part="track"]{
display:flex;margin:0;padding:0;list-style:none;
overflow-x:auto;overscroll-behavior-x:contain;
scroll-snap-type:x mandatory;scroll-behavior:smooth;scrollbar-width:none;
border-radius:0.875rem;
}
[data-vibeui-block="carousel-014"] [data-part="track"]::-webkit-scrollbar{display:none}
[data-vibeui-block="carousel-014"] [data-part="slide"]{
position:relative;flex:0 0 100%;min-width:0;
scroll-snap-align:center;scroll-snap-stop:always;
display:flex;flex-direction:column;justify-content:flex-end;gap:0.25rem;
aspect-ratio:16 / 9;padding:1rem 2.75rem;box-sizing:border-box;
background:
radial-gradient(90% 80% at 22% 18%,oklch(0.9 0.06 var(--vibeui-carousel-014-hue,250)),transparent 70%),
linear-gradient(150deg,oklch(0.74 0.1 var(--vibeui-carousel-014-hue,250)),oklch(0.44 0.11 var(--vibeui-carousel-014-hue,250)));
color:oklch(0.99 0.003 265);
}
[data-vibeui-block="carousel-014"] [data-part="title"]{margin:0;font-size:1.0625rem;font-weight:680;line-height:1.2}
[data-vibeui-block="carousel-014"] [data-part="text"]{margin:0;font-size:0.8125rem;line-height:1.45;color:oklch(0.94 0.01 265)}
/* Стрелки лежат в самом слайде и ведут на соседний якорь: текущая позиция
компоненту не нужна, а значит не нужен и JS. */
[data-vibeui-block="carousel-014"] [data-part="arrow"]{
position:absolute;top:50%;transform:translateY(-50%);
display:inline-flex;align-items:center;justify-content:center;
width:1.75rem;height:1.75rem;border-radius:9999px;
background:oklch(1 0 0 / 88%);color:oklch(0.24 0.02 265);
text-decoration:none;font-size:0.875rem;line-height:1;
}
[data-vibeui-block="carousel-014"] [data-part="arrow"]:hover{background:oklch(1 0 0)}
[data-vibeui-block="carousel-014"] [data-part="arrow"]:focus-visible{outline:2px solid var(--vibeui-carousel-014-accent);outline-offset:2px}
[data-vibeui-block="carousel-014"] [data-part="arrow"][data-way="prev"]{left:0.625rem}
[data-vibeui-block="carousel-014"] [data-part="arrow"][data-way="next"]{right:0.625rem}
[data-vibeui-block="carousel-014"] [data-part="hint"]{font-size:0.6875rem;color:var(--vibeui-carousel-014-muted);text-align:center}
@media (prefers-reduced-motion:reduce){
[data-vibeui-block="carousel-014"] [data-part="track"]{scroll-behavior:auto}
[data-vibeui-block="carousel-014"] *{animation:none!important;transition:none!important}
}
`
const DEFAULT_CARDS: Carousel014Card[] = [
{
title: "Сначала данные",
text: "Компонент получает массив и рисует ровно то, что в нём есть",
hue: 250,
},
{
title: "Потом раскладка",
text: "Ширина считается от контейнера, а не от окна браузера",
hue: 160,
},
{
title: "И только затем цвет",
text: "Палитра живёт в локальных переменных и не спорит с вашей темой",
hue: 35,
},
{
title: "Ничего лишнего",
text: "Ни рантайма, ни зависимостей: один файл в вашем проекте",
hue: 300,
},
]
/**
* Карусель на scroll-snap без JS: стрелки — якорные ссылки внутри слайдов.
* Один файл, ноль зависимостей, собственная палитра.
*/
export function Carousel014({
cards = DEFAULT_CARDS,
label = "Принципы",
idPrefix = "vibeui-carousel-014",
className,
style,
...props
}: Carousel014Props) {
return (
<>
<style href="vibeui-carousel-014" precedence="medium">
{STYLES}
</style>
<section
{...props}
data-vibeui-block="carousel-014"
aria-roledescription="карусель"
aria-label={label}
className={className}
style={style as CSSProperties}
>
<ul data-part="track">
{cards.map((card, index) => {
const previous = cards[(index - 1 + cards.length) % cards.length]
const next = cards[(index + 1) % cards.length]
return (
<li
data-part="slide"
key={card.title}
id={`${idPrefix}-${index + 1}`}
style={
{
"--vibeui-carousel-014-hue": card.hue ?? 250,
} as CSSProperties
}
>
<a
data-part="arrow"
data-way="prev"
href={`#${idPrefix}-${((index - 1 + cards.length) % cards.length) + 1}`}
aria-label={`Предыдущий слайд: ${previous.title}`}
>
‹
</a>
<h3 data-part="title">{card.title}</h3>
{card.text ? <p data-part="text">{card.text}</p> : null}
<a
data-part="arrow"
data-way="next"
href={`#${idPrefix}-${((index + 1) % cards.length) + 1}`}
aria-label={`Следующий слайд: ${next.title}`}
>
›
</a>
</li>
)
})}
</ul>
<p data-part="hint">
Листается пальцем, колесом и стрелками внутри кадра — без JavaScript.
</p>
</section>
</>
)
}