Slide Deck Shell
A 16:9 web presentation shell React component with proportional scaling, keyboard and touch navigation, four transitions, progress bar and page counter, speaker notes, URL-synced slide index and one-slide-per-page printing, plus nine built-in slide layouts.
Turn a presentation into a page of your site. Slides are laid out at a fixed design size (1280×720 by default), then scaled to the container with transform: scale(), so type and spacing never distort with screen size. Navigate with ← → Space Home End or a horizontal swipe; F toggles fullscreen, N opens speaker notes and ? shows every shortcut. Four transitions are available (slide / fade / zoom / none), and the bottom progress bar and rolling page counter update as you go. Turn on syncHash to write the slide number into the URL (#3 is slide 3); printing or Save as PDF gives exactly one slide per page. Nine layouts such as TitleSlide, StatsSlide and CodeSlide are included, so dropping them in gives you a complete deck.
Bring this into your project
Run this command in your project folder. The CLI adds the source and dependencies; then follow the usage example below.
npx webberui@latest add slide-deck-shell.enPlayground
Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.
<SlideDeckShell/>
The complete example includes the preview layout and data. Install the component first, then paste this file into your project.
Installation
npx webberui@latest add https://webberui.com/r/slide-deck-shell.en.jsonOr, once registries are configured in components.json, install it as @webberui/slide-deck-shell.
Usage
import {
SlideDeckShell,
TitleSlide,
ListSlide,
StatsSlide,
ClosingSlide,
} from "@/components/ui/slide-deck-shell";
<SlideDeckShell label="Q3 Product Review" transition="slide" syncHash>
<TitleSlide
eyebrow="2026 Q3"
title="Q3 Product Review"
subtitle="Three months, two major releases"
notes="Introduce yourself first"
/>
<ListSlide
title="Three headline results"
ordered
items={["Editor rewrite", "Team asset library", "Mobile review mode"]}
/>
<StatsSlide
title="Key numbers"
items={[
{ value: 12480, label: "Monthly active users" },
{ value: 64, suffix: "%", label: "30-day retention" },
]}
/>
<ClosingSlide title="Thank you" contact="hello@example.com" />
</SlideDeckShell>
// Or drive it with data (slides wins over children)
<SlideDeckShell
slides={[
{ id: "cover", content: <TitleSlide title="Cover" />, notes: "Notes" },
{ id: "end", content: <ClosingSlide title="The end" /> },
]}
/>Props
SlideDeckShell
| Prop | Type | Default | Description |
|---|---|---|---|
children | React.ReactNode | — | Slides (<Slide> or a built-in layout); use this or slides, slides wins |
slides | DeckSlideData[] | — | Data-driven slide array; children are ignored when provided |
index | number | — | Controlled current slide index (0-based); managed internally when omitted |
defaultIndex | number | 0 | Initial slide index in uncontrolled mode (0-based) |
onIndexChange | (index: number) => void | — | Called when the slide index changes (0-based) |
transition | "slide" | "fade" | "zoom" | "none" | "slide" | Slide transition; always becomes a short fade when the OS asks to reduce motion |
aspectRatio | number | 16 / 9 | Slide aspect ratio |
baseWidth | number | 1280 | Design width (px); slides are laid out at this size, then scaled to fit the container |
showProgress | boolean | true | Show the bottom progress bar |
showCounter | boolean | true | Show the slide counter |
syncHash | boolean | false | Sync the slide with the URL hash (#3 is slide 3); off by default so multiple decks on one page do not fight over the URL |
notes | boolean | — | Controlled speaker-notes panel state; managed internally when omitted |
defaultNotes | boolean | false | Whether the notes panel starts open in uncontrolled mode |
onNotesChange | (open: boolean) => void | — | Called when the notes panel opens or closes |
keyboard | "focus" | "global" | "focus" | focus only handles keys while the deck is focused or fullscreen; global listens on the whole window |
label | string | "Presentation" | Presentation name, used as the root aria-label |
labels | Partial<SlideDeckShellLabels> | — | UI string overrides |
className | string | — | Passed to the outermost container |
DeckSlideData
| Field | Type | Default | Description |
|---|---|---|---|
id | string | — | Stable identifier used as the React key; falls back to the index |
content | React.ReactNode | — | Slide content (usually a layout) |
notes | React.ReactNode | — | Speaker notes |
SlideDeckShellLabels
| Field | Type | Default | Description |
|---|---|---|---|
prev | string | "Previous slide" | Previous button |
next | string | "Next slide" | Next button |
fullscreen | string | "Fullscreen" | Enter-fullscreen button |
exitFullscreen | string | "Exit fullscreen" | Exit-fullscreen button |
notes | string | "Speaker notes" | Speaker-notes button and panel title |
shortcuts | string | "Keyboard shortcuts" | Shortcuts button and overlay title |
close | string | "Close" | Close button of the shortcuts overlay |
progress | string | "Presentation progress" | Accessible name of the progress bar |
noNotes | string | "No notes for this slide" | Shown when the current slide has no notes |
deck | string | "presentation" | aria-roledescription of the root element |
slide | string | "slide" | aria-roledescription of each slide |
page | (current: number, total: number) => string | Slide N of M | Page description, used for each slide's aria-label and the page-change announcement |
Slide layouts
Every layout accepts notes (speaker notes), background (any CSS background value) and className.
| Component | Main props | Description |
|---|---|---|
Slide | children | Blank slide laid out edge to edge at the design size; bring your own content |
TitleSlide | eyebrow, title, subtitle | Title slide |
SectionSlide | number, title, description | Section divider |
TwoColumnSlide | title, left, right, ratio (default [1, 1]) | Two columns |
ImageFullSlide | src, alt, title, caption | Full-bleed image (your own URL) or gradient with a caption card |
StatsSlide | title, items: DeckStat[], locale (default "en-US") | Figures that count up from 0 over 1.2s on entry |
QuoteSlide | quote, author, role | Quote slide |
ListSlide | title, items, ordered | List whose items fade in, staggered 80ms |
CodeSlide | title, code, language, filename, copyLabel, copiedLabel | Code with monospace text, line numbers and a copy button |
ClosingSlide | title, subtitle, contact | Closing slide |
DeckStat
| Field | Type | Default | Description |
|---|---|---|---|
value | number | — | Target value |
label | React.ReactNode | — | Figure label |
prefix | string | — | Prefix (e.g. NT$) |
suffix | string | — | Suffix (e.g. %) |
How it works
- Scaling: a ResizeObserver measures the container and the scale is the smaller of container width ÷
baseWidthand container height ÷ design height, so fullscreen letterboxes automatically. Inside a slide you can lay things out with px and regular Tailwind type sizes. - Transitions:
AnimatePresence mode="wait"plays exit then enter; the whole page change takes--wb-deck-duration(default400ms) on the--wb-deck-easecurve (defaultcubic-bezier(0.22,1,0.36,1)). Both variables are read from CSS before each page change and can be overridden on any ancestor. The accent colour is--wb-deck-accent, which follows the text colour by default. - Keyboard: with the default
keyboard="focus", arrow keys and Space are only handled after the deck is clicked or focused with Tab, so page scrolling is never hijacked; in fullscreen the whole window is listened to. Keys typed into text fields are always ignored. - Touch: a horizontal swipe over 50px, or faster than 0.3px/ms, changes slides; vertical swipes scroll the page (
touch-action: pan-y). - Fullscreen: F or the button calls the Fullscreen API; the button is hidden when the browser does not support it. Esc closes the shortcuts overlay first; in fullscreen the browser handles Esc to exit.
- Speaker notes: shown below the deck, or as a right-hand sidebar in fullscreen. In
childrenmode they are collected from each child'snotesprop. - Printing: under
@media printthe controls are hidden, scaling and transitions are off and every slide is printed in order on paper sized to the slide, so Save as PDF gives exactly one slide per page. The@pagerule applies to the whole document while the component is mounted, so a dedicated route for the deck is recommended. - No PPTX export: this is a front-end-only component. If you need PPTX, print to PDF from the browser or convert separately.
CodeSlideonly does monospace text, line numbers and a copy button, without syntax highlighting; swap in WebberUI'scode-blockif you need highlighting.
Accessibility
- The root is
role="region"witharia-roledescription="presentation"and is named bylabel; each slide isrole="group"witharia-roledescription="slide"and anaria-labelsuch as "Slide 3 of 8" - Only the current slide is rendered; a slide that is exiting is
inertand hidden from assistive technology - On every page change an
aria-live="polite"region announces "Slide N of M"; the bottom progress bar isrole="progressbar" - Every control is
type="button"with anaria-labeland a focus-visible ring; the notes button exposesaria-pressed - The shortcuts overlay is
role="dialog"witharia-modal; focus moves to the close button, stays inside the dialog and returns where it was when closed - When the OS asks to reduce motion, every transition becomes a 150ms fade, figures show their final value immediately, list items appear without a stagger and the page counter does not roll
Pain Point Orbit
As you type into the input, surrounding pain-point cards crowd inward one by one; on submit they scatter away — a marketing visual for the pain of fragmented tools.
Hero Collection 01
Three animated hero sections — a drifting gradient mesh, a canvas particle field, and per-character split text — sharing one content API, with the content revealing in sequence as it enters the viewport.