Presentations

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.en

New project or installation trouble? Start here →

Playground

Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.

400
Active edited usageDefault props
<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.json

Or, 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

PropTypeDefaultDescription
childrenReact.ReactNode—Slides (<Slide> or a built-in layout); use this or slides, slides wins
slidesDeckSlideData[]—Data-driven slide array; children are ignored when provided
indexnumber—Controlled current slide index (0-based); managed internally when omitted
defaultIndexnumber0Initial 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
aspectRationumber16 / 9Slide aspect ratio
baseWidthnumber1280Design width (px); slides are laid out at this size, then scaled to fit the container
showProgressbooleantrueShow the bottom progress bar
showCounterbooleantrueShow the slide counter
syncHashbooleanfalseSync 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
notesboolean—Controlled speaker-notes panel state; managed internally when omitted
defaultNotesbooleanfalseWhether 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
labelstring"Presentation"Presentation name, used as the root aria-label
labelsPartial<SlideDeckShellLabels>—UI string overrides
classNamestring—Passed to the outermost container

DeckSlideData

FieldTypeDefaultDescription
idstring—Stable identifier used as the React key; falls back to the index
contentReact.ReactNode—Slide content (usually a layout)
notesReact.ReactNode—Speaker notes

SlideDeckShellLabels

FieldTypeDefaultDescription
prevstring"Previous slide"Previous button
nextstring"Next slide"Next button
fullscreenstring"Fullscreen"Enter-fullscreen button
exitFullscreenstring"Exit fullscreen"Exit-fullscreen button
notesstring"Speaker notes"Speaker-notes button and panel title
shortcutsstring"Keyboard shortcuts"Shortcuts button and overlay title
closestring"Close"Close button of the shortcuts overlay
progressstring"Presentation progress"Accessible name of the progress bar
noNotesstring"No notes for this slide"Shown when the current slide has no notes
deckstring"presentation"aria-roledescription of the root element
slidestring"slide"aria-roledescription of each slide
page(current: number, total: number) => stringSlide N of MPage 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.

ComponentMain propsDescription
SlidechildrenBlank slide laid out edge to edge at the design size; bring your own content
TitleSlideeyebrow, title, subtitleTitle slide
SectionSlidenumber, title, descriptionSection divider
TwoColumnSlidetitle, left, right, ratio (default [1, 1])Two columns
ImageFullSlidesrc, alt, title, captionFull-bleed image (your own URL) or gradient with a caption card
StatsSlidetitle, items: DeckStat[], locale (default "en-US")Figures that count up from 0 over 1.2s on entry
QuoteSlidequote, author, roleQuote slide
ListSlidetitle, items, orderedList whose items fade in, staggered 80ms
CodeSlidetitle, code, language, filename, copyLabel, copiedLabelCode with monospace text, line numbers and a copy button
ClosingSlidetitle, subtitle, contactClosing slide

DeckStat

FieldTypeDefaultDescription
valuenumber—Target value
labelReact.ReactNode—Figure label
prefixstring—Prefix (e.g. NT$)
suffixstring—Suffix (e.g. %)

How it works

  • Scaling: a ResizeObserver measures the container and the scale is the smaller of container width ÷ baseWidth and 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 (default 400ms) on the --wb-deck-ease curve (default cubic-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 children mode they are collected from each child's notes prop.
  • Printing: under @media print the 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 @page rule 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.
  • CodeSlide only does monospace text, line numbers and a copy button, without syntax highlighting; swap in WebberUI's code-block if you need highlighting.

Accessibility

  • The root is role="region" with aria-roledescription="presentation" and is named by label; each slide is role="group" with aria-roledescription="slide" and an aria-label such as "Slide 3 of 8"
  • Only the current slide is rendered; a slide that is exiting is inert and hidden from assistive technology
  • On every page change an aria-live="polite" region announces "Slide N of M"; the bottom progress bar is role="progressbar"
  • Every control is type="button" with an aria-label and a focus-visible ring; the notes button exposes aria-pressed
  • The shortcuts overlay is role="dialog" with aria-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