Carousel
Anchor Deck
A carousel with arrows and no JS at all: each slide carries its own pair of arrows pointing at the neighbouring anchors, so the component never needs to know the current position.
- carousel
- scroll-snap
- no-js
- anchors
Preview
Use it with AI
- 1. Copy the link.
- 2. Write to your agent in your own words and drop the link into the sentence.
- 3. The agent opens the link and installs the component from the registry.
put this in the header: https://vibeui.ru/c/carousel-014?lang=en
- ‹
Сначала данные
Компонент получает массив и рисует ровно то, что в нём есть
› - ‹
Потом раскладка
Ширина считается от контейнера, а не от окна браузера
› - ‹
И только затем цвет
Палитра живёт в локальных переменных и не спорит с вашей темой
› - ‹
Ничего лишнего
Ни рантайма, ни зависимостей: один файл в вашем проекте
›
Листается пальцем, колесом и стрелками внутри кадра — без JavaScript.
Show the full instructions
What the link says, spelled out. Use it when your agent cannot open links — paste this text instead.
# Install and place "carousel-014" (Anchor Deck) from VibeUI
## 1. Install first — do not skip, do not recreate
Run this exact command before writing any code:
npx shadcn@latest add https://vibeui.ru/r/carousel-014.json
Registry item: https://vibeui.ru/r/carousel-014.json
Installs to: components/vibeui/carousel-014.tsx (the exact path follows this project's components.json aliases).
npm dependencies: none.
Registry dependencies: none.
Install it from the registry. Do not recreate it from the description,
do not substitute a similar component from another library, and do not
rewrite it to match the project's existing style.
## 2. What it is
A carousel with arrows and no JS at all: each slide carries its own pair of arrows pointing at the neighbouring anchors, so the component never needs to know the current position.
A scroll-snap carousel with no JS: the in-slide arrows are anchor links to the neighbours. Zero dependencies, one file, no client JS.
## 3. How to use it
import { Carousel014 } from "@/components/vibeui/carousel-014"
<Carousel014
label="Principles"
idPrefix="principles"
cards={[
{ title: "Data first", text: "The component draws what it receives", hue: 250 },
{ title: "Layout next", text: "Width comes from the container", hue: 160 },
]}
/>
Read the installed file for the full prop list.
## 4. Where to place it
This is a small inline component. Put it exactly where the user asked,
inside the existing markup. Do not create a new page, section or
wrapper for it. If a similar control already sits in that spot,
replace it instead of adding a second one.
Placement: ___
(The user fills this line in. If it is still blank, ask where to put it
instead of guessing.)
## 5. Keep exactly as installed
- the local --vibeui-carousel-014-* palette — do not swap it for your theme tokens
- the arrows inside the slides: only that way are "previous" and "next" known without a position
- scroll-snap-stop:always — otherwise a fast swipe skips two slides at once
- the ids built from idPrefix: two blocks with the same anchors break each other's links
- arrow labels naming the neighbouring slide: a bare "next" says nothing
- the native scrolling: touch, wheel, Tab and keys work without handlers
## 6. You may change
- the slides through cards: title, text and hue
- the carousel label through label
- the id base through idPrefix
- the frame proportions in the aspect-ratio rule of [data-part="slide"]
## 7. Rules
- An anchor jump adds a history entry: the back button returns to the previous slide.
- The component does not know the active slide, so dot indicators are impossible here.
- The deck loops through the links: the last arrow points back at the first slide.
- Do not lift the CSS variables into globals.css: the component has to stay a single file.
## 8. Verify
- it renders with no console errors;
- it looks like the preview on the VibeUI page you copied this from;
- if it does not, you changed something listed in section 5 — put it back.For developers
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, чтобы два блока на одной странице не перебили якоря друг друга.
Component source
The same file your agent installs. Here in case you would rather copy it by hand.
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>
</>
)
}