slidedeck 1.0
A carousel that lets
the browser do the scrolling.
Compose it from a few React components, style every part with your own CSS, or hand it to your agent. The momentum, the snapping and the focus scrolling are the browser's own.
pnpm add @slidedeck/reactThe browser scrolls. slidedeck keeps count.
A real scroll container
Every deck is a scroll container with CSS scroll snap: touch, trackpad, wheel and keyboard move it natively, and slidedeck adds mouse drag.
slidedeck keeps count
It tracks where the deck comes to rest, and Prev, Next and the dots ask the browser to scroll there.
Works with no CSS
The quickstart is a working deck before you write a single rule.
8.84 KB
for a carousel with Prev and Next
min+gzip, React external
Measured by pnpm compare in slidedeck's repo, and its CI fails when the number goes stale.
Unstyled, your look.
slidedeck needs no stylesheet. Its defaults and its optional theme sit in :where(), so any rule of yours wins. Every part carries a name, like data-slidedeck-next or data-slidedeck-dots, for your CSS or your tokens to reach. Here is one live deck, shown twice: the markup is the same, and only the CSS changes.
.soft :has(> [data-slidedeck-viewport]) {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 12px;
padding: 20px;
border-radius: 32px;
background: linear-gradient(135deg, #ffe4ec, #e4e0ff);
color: #3b2a4d;
font-family: ui-rounded, "SF Pro Rounded", system-ui, sans-serif;
}
.soft [data-slidedeck-viewport] {
flex: 0 0 100%;
gap: 12px;
border-radius: 24px;
}
.soft [data-slidedeck-slide] {
width: 82%;
scroll-snap-align: center;
}
.soft [data-slidedeck-slide] > div {
display: grid;
align-content: center;
gap: 4px;
min-height: 150px;
padding: 24px;
border-radius: 24px;
background: #fff;
}
.soft [data-slidedeck-slide] strong {
font-size: 26px;
line-height: 1.1;
}
.soft [data-slidedeck-slide] span {
color: #6a5880;
}
.soft [data-slidedeck-prev],
.soft [data-slidedeck-next] {
min-height: 44px;
padding: 0 18px;
border: 0;
border-radius: 999px;
background: #3b2a4d;
color: #fff;
font: inherit;
font-weight: 600;
cursor: pointer;
}
.soft [data-slidedeck-prev]:disabled,
.soft [data-slidedeck-next]:disabled {
opacity: .35;
cursor: default;
}
.soft [data-slidedeck-dots] {
display: flex;
}
.soft [data-slidedeck-dots] button {
display: grid;
place-items: center;
width: 24px;
height: 44px;
padding: 0;
border: 0;
background: none;
cursor: pointer;
}
.soft [data-slidedeck-dots] button::before {
content: "";
width: 0;
border: 4px solid #8a76a8;
border-radius: 999px;
}
.soft [data-slidedeck-dots] [aria-current="true"]::before {
width: 10px;
border-color: #3b2a4d;
}
.soft [data-slidedeck-counter] {
margin-left: auto;
font-weight: 600;
font-variant-numeric: tabular-nums;
}
.soft :focus-visible {
outline: 2px solid #3b2a4d;
outline-offset: 2px;
}.stark :has(> [data-slidedeck-viewport]) {
position: relative;
display: flex;
flex-wrap: wrap;
border: 3px solid #000;
background: #fff;
color: #000;
font-family: ui-monospace, Menlo, monospace;
text-transform: uppercase;
}
.stark [data-slidedeck-viewport] {
flex: 0 0 100%;
border-bottom: 3px solid #000;
}
.stark [data-slidedeck-slide] > div {
display: grid;
align-content: end;
gap: 8px;
min-height: 190px;
box-sizing: border-box;
padding: 20px;
background: #ffd60a;
}
.stark [data-slidedeck-slide] strong {
font-size: 30px;
line-height: 1;
letter-spacing: -.04em;
}
.stark [data-slidedeck-slide] span {
font-size: 13px;
letter-spacing: .1em;
}
.stark [data-slidedeck-prev],
.stark [data-slidedeck-next] {
min-height: 48px;
padding: 0 14px;
border: 0;
border-right: 3px solid #000;
border-radius: 0;
background: #fff;
color: #000;
font: inherit;
font-size: 13px;
font-weight: 700;
cursor: pointer;
}
.stark [data-slidedeck-prev]:hover:enabled,
.stark [data-slidedeck-next]:hover:enabled {
background: #000;
color: #fff;
}
.stark [data-slidedeck-prev]:disabled,
.stark [data-slidedeck-next]:disabled {
color: #767676;
cursor: default;
}
.stark [data-slidedeck-dots] {
display: flex;
flex: 1 1 96px;
gap: 4px;
padding: 0 12px;
}
.stark [data-slidedeck-dots] button {
flex: 1;
min-width: 24px;
height: 48px;
padding: 0;
border: 0;
background: none;
cursor: pointer;
}
.stark [data-slidedeck-dots] button::before {
content: "";
display: block;
border-top: 3px solid #b0b0b0;
}
.stark [data-slidedeck-dots] [aria-current="true"]::before {
border-top: 9px solid #000;
}
.stark [data-slidedeck-counter] {
position: absolute;
top: 0;
right: 0;
padding: 6px 10px;
background: #000;
color: #fff;
font-size: 13px;
font-weight: 700;
font-variant-numeric: tabular-nums;
}
.stark :focus-visible {
outline: 3px solid #000;
outline-offset: -7px;
}Small, leans on the browser.
The browser already knows how to scroll. slidedeck builds on that instead of rebuilding it, so a carousel with Prev and Next is 8.84 KB, min+gzip, with React external.
Native scroll snap
Momentum, snapping and focus scrolling are the browser's own. slidedeck tracks where the deck rests and asks the browser to move.
Effects you import
Fade and curve each have an entry point of their own. A deck that imports neither ships none of their code.
Rendered on the server
Slides, Prev and Next are in the HTML before any script runs, and React Server Components render the deck straight from a server file.
Built for agents.
Whether you write the code or your coding agent does, slidedeck leaves less to guess.
A few named parts
A deck is Deck.Root, Deck.Viewport and Deck.Slide, plus the controls you pick. The quickstart below is the whole of one.
Typed all the way
slidedeck is written in TypeScript and ships its types, so your editor and your agent see every prop.
Mistakes explain themselves
Pass index and defaultIndex together, or index without onIndexChange, and in development slidedeck says what to pass instead.
import * as Deck from '@slidedeck/react';
export function Featured() {
return (
<Deck.Root aria-label="Featured products">
<Deck.Viewport>
<Deck.Slide>Slide one</Deck.Slide>
<Deck.Slide>Slide two</Deck.Slide>
<Deck.Slide>Slide three</Deck.Slide>
</Deck.Viewport>
<Deck.Prev />
<Deck.Next />
<Deck.Dots />
<Deck.Counter />
</Deck.Root>
);
}Accessible by default.
slidedeck takes care of the keyboard, focus and screen readers for you. Each line below is backed by a test in slidedeck's own repo.
Try the examples ›Named for screen readers
A labelled carousel region, each slide a group labelled "n of m".
Ready for the keyboard
Arrow keys move the focused deck, and every dot is a button the keyboard reaches.
Focus brings it into view
Tab into an off-screen slide and it scrolls into view.
Announced politely
A polite live region announces your moves, never autoplay's.
Calm under reduced motion
Under reduced motion, moves jump instead of gliding, and autoplay starts stopped.
slidedeck is part of deck.cool, a family of unstyled React packages that look like you and work like the browser. Meet the family ›