Deck is a stylesheet first. The JavaScript adds behaviour that CSS cannot express —
a date picker, a toast queue, a data grid — and it is optional: a page with no
scripts at all still gets every component that HTML and CSS can carry on their own.
Most people will never call a method on this page, because
the attributes do it for them.
Loading it
Three files, in order, and only the first is required.
deck.js defines the global and the core components.
deck-extras.js adds the carousel, drawer, mega menu, copy button, QR
encoder and the rest. deck-adapters.js is the bridge to optional
third-party libraries and does nothing at all when none of them are present.
<script src="/assets/deck/deck.js" defer></script>
<script src="/assets/deck/deck-extras.js" defer></script>
<script src="/assets/deck/deck-adapters.js" defer></script>
<!-- or all three in one request -->
<script src="/assets/deck/deck.bundle.min.js" defer></script>
Each of the optional two extends Deck.init rather than replacing it, so
load order matters and the extras must come after the core. Loading extras without
the core logs a warning and stops rather than throwing.
Deck is exposed on window, on globalThis, and
as a CommonJS export, so it is reachable whether it was loaded as a classic script, a
module, or through a bundler.
deck-icons.svg is expected to sit beside deck.js and is
found automatically, because the URL is resolved against the script tag's own
src at execution time. If your build puts them in different places, set
data-deck-icons on the tag.
The data attributes
This is how most people will use the JavaScript: by not writing any. Add an
attribute in the markup and Deck.init() wires it on load. There are
15 of them.
Attributes that wire a component with no code
Attribute
Put it on
What it does
From
data-deck-icons
On the <script> tag
Overrides where the icon sprite is loaded from. Without it the sprite is resolved against the script's own URL, so it follows the bundle wherever it is installed.
deck.js
data-deck-datepicker
A .datefield
Turns it into a date picker. Takes data-mode, data-format, data-min, data-max, data-presets, data-locale.
deck.js
data-deck-combo
A .combo
Turns it into a combobox. Takes data-multi and data-create.
deck.js
data-deck-grid
A .dg-wrap
Wires the data grid inside it: sorting, selection, column resizing.
deck.js
data-deck-theme
A <button>
Makes it a light and dark toggle. No code, no handler, and the choice persists.
deck.js
data-deck-tick
Any element with a number in it
Counts the number up when it scrolls into view.
deck.js
data-deck-flip
A trigger
Flips a card on click. The value is a selector for the card; without one it flips the nearest.
deck.js
data-deck-anchor
A popover trigger
Anchors the popover to the trigger. Set by Deck on popover triggers it wires, and read by the Floating UI adapter where CSS anchor positioning is missing.
deck.js, deck-adapters.js
data-deck-btt
A .back-to-top
Marks the link as wired and is the hook the visibility rule keys off. Deck adds it during init, so you rarely write it yourself.
deck.js
data-deck-carousel
A .carousel
Wires the slides, arrows and dots. Takes data-autoplay in milliseconds.
deck-extras.js
data-deck-drawer
A trigger
Opens the drawer named by its value.
deck-extras.js
data-deck-mega
A trigger
Opens the mega menu named by its value.
deck-extras.js
data-deck-copy
A .copy-btn
Copies on click. The value is a selector for what to copy; with no value it takes the text of the .copy-value beside it.
deck-extras.js
data-deck-qr
Any element
Replaces its content with a QR code. The value is what to encode, falling back to the element's own text. Takes data-ecl and data-caption.
deck-extras.js
data-deck-sortable
A list
Makes its children drag-reorderable. The value is a group name, for dragging between lists.
deck-adapters.js
A working theme switch, with no JavaScript written by you
These turn up in dev tools and look like something you were meant to write. They are
not: they are the once-guards that make Deck.init() safe to call
repeatedly over the same subtree.
Internal marker attributes
Attribute
Meaning
data-deck-wired
The once-guard in deck.js. If it is on an element, that element has already been wired and init will skip it.
data-deck-x
The same guard in deck-extras.js.
data-deck-a
The same guard in deck-adapters.js.
data-deck-anchored
Marks a popover trigger whose anchor name has already been assigned.
Core methods
On window.Deck, from deck.js. Available on every page that
loads Deck at all.
Deck.init(root = document)
Finds every element inside root carrying a data-deck-* attribute and wires its behaviour. Runs itself once on DOMContentLoaded; call it again after you inject markup.
Parameters for Deck.init(root = document)
Parameter
Type
Meaning
root
Element | Document
The subtree to scan. Defaults to the whole document.
Returns
The Deck object, so calls chain.
When what it needs is missing
Nothing observes the DOM — there is no MutationObserver — so HTML added after load is inert until you call this. Calling it twice is safe: every wiring sets a once-guard on the element (data-deck-wired, data-deck-x, data-deck-a, or a _deck property) and skips anything already wired, so re-running it over a whole page only costs the query.
const res = await fetch('/rows?page=2');
list.insertAdjacentHTML('beforeend', await res.text());
Deck.init(list); // only the new rows get wired
Defined in src/js/deck.js.
Deck.toast(opts)
Pushes a notification into the toast region and returns a handle to it. A string is shorthand for { title: string }.
Parameters for Deck.toast(opts)
Parameter
Type
Meaning
opts
string | object
The message, or the options below.
opts.title
string
The bold first line. Escaped before insertion.
opts.text
string
A second line of detail. Escaped.
opts.kind
string
One of good, bad, warn, info, loading, or omitted for neutral. Chooses the icon, the tint, and whether the element is announced as an alert.
opts.duration
number
Milliseconds before it dismisses itself. Default 5000. 0 means it stays until something dismisses it.
opts.actions
Array
Buttons, as { label, onClick, close }. onClick receives the handle. The toast closes after the action unless close is false.
opts.dismissible
boolean
Whether to render the close button. Default true.
opts.onDismiss
Function
Called with the handle once it has been dismissed, however that happened.
Returns
A handle: { node, opts, dismiss(), update(patch) }. node is the live element, opts is the merged options, dismiss() removes it now, and update(patch) merges new options in, repaints the body and restarts the timer — which is what turns a loading toast into a result rather than stacking a second toast on top of it.
When what it needs is missing
No markup is required. If the page has no .toast-region, one is created and appended to <body> on the first call. The element is given role="alert" when kind is bad and role="status" otherwise, so it is announced without any ARIA on your side. Three are visible at once; the rest are kept in the DOM with aria-hidden, and pushing a ninth dismisses the oldest.
Deck.toast('Order #1042 shipped');
Deck.toast({
kind: 'warn',
title: 'Project archived',
text: 'You can still restore it from the archive.',
duration: 8000,
actions: [{ label: 'Undo', onClick: restore }]
});
// The handle is the point: one toast, two states
const t = Deck.toast({ kind: 'loading', title: 'Submitting…', duration: 0 });
try {
await save();
t.update({ kind: 'good', title: 'Submitted', duration: 4000 });
} catch (err) {
t.update({ kind: 'bad', title: 'Could not submit', text: err.message, duration: 0 });
}
Defined in src/js/deck.js.
Deck.toasts
The queue behind Deck.toast(), exposed so you can act on all of them at once. .items is the live array, newest first; .clear() dismisses every one; .max is how many are visible at a time and .stacked whether they pile up or list out.
Returns
The queue object. Deck.toasts.push(opts) is what Deck.toast() calls.
When what it needs is missing
Setting .max or .stacked after toasts are already on screen affects the next reflow rather than repainting immediately. Change them at startup.
Reads or sets the colour theme by writing data-theme on the <html> element.
Parameters for Deck.theme(mode?)
Parameter
Type
Meaning
mode
string
'light' or 'dark'. Omit to read.
Returns
With no argument, the current theme, or 'auto' when no attribute is set and the operating system decides. With an argument, the mode you passed.
When what it needs is missing
The choice is persisted to localStorage under deck-theme inside a try/catch, so private browsing or blocked storage costs you the persistence and nothing else — the attribute is still set and the page still changes. On the next load the saved value is applied before Deck.init() runs, so there is no flash of the wrong theme.
Deck.theme(); // 'auto', 'light' or 'dark'
Deck.theme('dark');
// A button with data-deck-theme does this for you, with no code at all
Defined in src/js/deck.js.
Deck.hue(n)
Sets --hue-brand as an inline style on <html>. Every colour derived from the brand hue — fills, borders, focus rings, shadows, chart series — follows immediately.
Parameters for Deck.hue(n)
Parameter
Type
Meaning
n
number | string
An OKLCH hue angle, 0 to 360.
Returns
Nothing.
When what it needs is missing
Not persisted and not validated. A value outside 0–360 wraps the way OKLCH does rather than erroring. For per-tenant theming set the property server-side instead, so the first paint is already correct — Deck::theme() in the PHP helper emits exactly that.
// Live preview from a slider
range.addEventListener('input', () => Deck.hue(range.value));
Defined in src/js/deck.js.
Deck.dir(value?)
Reads or sets the text direction on <html>. Deck is written entirely in logical properties, so this is the whole of a right-to-left flip — there is no second stylesheet to load.
Parameters for Deck.dir(value?)
Parameter
Type
Meaning
value
string
'ltr' or 'rtl'. Omit to read.
Returns
The current direction, defaulting to 'ltr' when the attribute is absent.
When what it needs is missing
Persisted to localStorage under deck-dir in a try/catch, and restored before init on the next load. Note that this sets the direction only — it does not translate anything, and a right-to-left page in English is a demo rather than a localisation.
Deck.dir('rtl');
document.querySelectorAll('.mirror-rtl').length; // glyphs that flip with it
Defined in src/js/deck.js.
Deck.locale(tag?)
Month and weekday names, and the first day of the week, from Intl. This is what the date picker reads, and it is public because the same data is usually wanted beside it.
Parameters for Deck.locale(tag?)
Parameter
Type
Meaning
tag
string
A BCP 47 tag such as 'de-AT'. Omit to use the page.
Returns
{ locale, months, monthsShort, days, weekStart, long, full }. days runs Sunday to Saturday; weekStart is the index the locale actually begins on — 0 in the US, 1 across most of Europe. long and full are Intl.DateTimeFormat instances, ready to .format().
When what it needs is missing
With no tag it falls back through <html lang>, navigator.language, then en. Where Intl.Locale.getWeekInfo() is unsupported — Firefox, at the time of writing — weekStart comes back as 0 rather than throwing, so a European page there starts its weeks on Sunday until that lands. Results are cached per tag.
Wraps a DOM change so the browser tweens between the before and after states, using the View Transition API.
Parameters for Deck.transition(update, opts = {})
Parameter
Type
Meaning
update
Function
The mutation to run. It may be async; the transition waits for the returned promise.
opts.direction
string
Written to data-vt on <html> for the duration, so your CSS can animate forward and back differently.
opts.name
string
A label for the transition, for your own bookkeeping.
Returns
The ViewTransition, with .ready and .finished promises.
When what it needs is missing
This is the one to know. Where document.startViewTransition does not exist, or the reader has asked for reduced motion, the update runs immediately and the return value is a stub with the same shape — { finished, ready }, both already resolved. So await Deck.transition(fn).finished is safe to write unconditionally, and you never need to feature-detect at the call site.
Adds a one-shot animation class, waits for it, then cleans up after itself. It also removes the class and forces a reflow first, so playing the same animation twice in a row actually plays twice.
Parameters for Deck.play(node, className)
Parameter
Type
Meaning
node
Element
The element to animate.
className
string
An animation class such as shake, flash-good or pop.
Returns
A Promise resolving to the node when the animation ends. is-animating is on the element for the duration, which is what the motion stylesheet keys its will-change hints off.
When what it needs is missing
If node is null or undefined it resolves immediately rather than throwing, so an optional target does not need a guard. There is also a 2000ms safety timeout: a class that triggers no animation at all — a typo, or a rule switched off under reduced motion — still resolves and still cleans up, rather than leaving a promise pending forever.
Opens or closes a panel with an animated height, with no measuring. It adds .expand and toggles .is-open; the animation comes from interpolate-size in the stylesheet.
Parameters for Deck.toggle(node, force?)
Parameter
Type
Meaning
node
Element
The panel.
force
boolean
Open or close explicitly instead of toggling.
Returns
The resulting open state, as a boolean.
When what it needs is missing
The height animation needs interpolate-size: allow-keywords, which Deck opts into on :root. Where the browser does not support it the panel still opens and closes correctly — it simply snaps instead of sliding. Nothing is measured and nothing is read back, so this cannot desynchronise from the content the way a JavaScript height animation does.
Toggles .is-flipped, which turns a flip card over.
Parameters for Deck.flip(node, force?)
Parameter
Type
Meaning
node
Element
The card.
force
boolean
Set the state explicitly rather than toggling.
Returns
The resulting state, as a boolean.
When what it needs is missing
Purely a class toggle; the rotation is CSS. Under reduced motion the stylesheet cross-fades the faces instead of rotating them, so the method behaves the same and the appearance changes. An element with data-deck-flip does this on click without any code.
Deck.flip(card);
Deck.flip(card, false); // back to the front, whatever it was showing
Defined in src/js/deck.js.
Deck.advance(stack)
Advances a depth stack — the swipeable pile of cards — by one, by dispatching a deck:advance event on it.
Parameters for Deck.advance(stack)
Parameter
Type
Meaning
stack
Element
The .stack-depth element.
Returns
Nothing.
When what it needs is missing
It dispatches an event rather than manipulating the pile, so it works whether the stack was wired by Deck.init() or by you. If nothing is listening the event goes nowhere and no error is raised.
Rotates a 3D cube to a named face by setting data-face.
Parameters for Deck.face(cube, name)
Parameter
Type
Meaning
cube
Element
The .cube element.
name
string
One of front, back, left, right, top, bottom.
Returns
The name you passed.
When what it needs is missing
An unrecognised name is written to the attribute and no rule matches it, so the cube stays where it was. The rotation is a CSS transition on the attribute selector, so this is inert without the stylesheet and silent under reduced motion.
Deck.face(cube, 'right');
Defined in src/js/deck.js.
Deck.reduced()
Whether the reader has asked their system to reduce motion. Deck checks this itself everywhere it animates; it is public so your own code can make the same decision from the same place.
Returns
true when prefers-reduced-motion: reduce matches.
When what it needs is missing
Evaluated on each call rather than cached, so it follows a preference changed mid-session. A browser with no matchMedia support would throw, but every browser Deck targets has it.
if (!Deck.reduced()) {
confetti();
}
Defined in src/js/deck.js.
Extras and adapters
Added to the same global by deck-extras.js and
deck-adapters.js. If you are not loading those files, these are
undefined.
Deck.copy(text)
Writes a string to the clipboard.
Parameters for Deck.copy(text)
Parameter
Type
Meaning
text
string
What to copy.
Returns
A Promise<boolean> — whether the copy succeeded.
When what it needs is missing
The async clipboard API is only available on secure origins, so on plain http and in older engines this falls back to a hidden textarea and document.execCommand("copy"). That fallback is deprecated and may eventually stop working, which is why the return value is a boolean worth checking rather than something to ignore. It never throws.
const ok = await Deck.copy(input.value);
Deck.toast(ok ? 'Copied' : { kind: 'bad', title: 'Could not copy' });
Defined in src/js/deck-extras.js.
Deck.qr.svg(text, ecl = "M")
Encodes a string as a QR code and returns it as SVG markup. The encoder is Deck's own — there is no library behind it and no network call, so this works offline and leaks nothing.
Parameters for Deck.qr.svg(text, ecl = "M")
Parameter
Type
Meaning
text
string
What to encode.
ecl
string
Error correction level: L, M, Q or H. Higher levels survive more damage and hold less data.
Returns
An SVG string, sized in module units with a viewBox, so it scales to whatever box you put it in. Horizontal runs are merged into single rects, which keeps the markup small.
When what it needs is missing
Throws where the text is too long for the largest version the encoder supports at that error correction level — the message says so. An element with data-deck-qr catches that and renders the message in place instead, so a too-long value degrades to readable text rather than a blank box.
What optional libraries are on the page, and what is actually doing the work. .available is a getter returning a boolean per library; .report() answers the more useful question.
Returns
.available gives { floating, quill, tiptap, chart, sortable, lucide }. .report() gives { placement, editor, charts, dragDrop, icons, virtualization }, each a sentence naming the implementation in use.
When what it needs is missing
This is the point of it: every adapter has a built-in fallback, so nothing here being present is a supported configuration. Placement falls back from CSS anchor positioning to Floating UI to Deck's own; the editor from Tiptap to Quill to execCommand; charts from Chart.js to Deck's CSS charts; drag and drop from SortableJS to the native HTML API. Paste Deck.adapters.report() into a console when a component behaves differently on two pages — it is usually this.
Creates a Chart.js chart with Deck's series palette already applied, so the chart matches the theme and follows --hue-brand like everything else.
Parameters for Deck.chart(canvas, config)
Parameter
Type
Meaning
canvas
HTMLCanvasElement
The canvas to draw into.
config
object
A Chart.js config. Datasets that already set borderColor or backgroundColor are left alone.
Returns
The Chart instance.
When what it needs is missing
This method does not exist unless Chart.js is loaded. It is defined inside the adapter's Chart.js branch, so on a page without the library Deck.chart is undefined and calling it throws. Guard with Deck.adapters.available.chart, or use Deck's CSS charts, which need no JavaScript at all. The same applies to Deck.chartTheme, which exposes { apply, repaint, series } and repaints every chart when the theme or the hue changes.
if (Deck.adapters.available.chart) {
Deck.chart(canvas, { type: 'line', data });
}
Defined in src/js/deck-adapters.js.
Properties
Properties on the Deck global
Property
Type
Notes
Deck.iconSprite
string
The URL every <use> Deck generates points at. Resolved from
the script tag at load time. Assigning to it later works, but only affects
icons rendered after the assignment — anything already on the page keeps the
URL it was built with.
{ apply, repaint, series }. Only defined when Chart.js was on the
page as the adapter loaded.
What still works with the scripts removed
Worth knowing before you reach for any of the above. The modal, drawer, sheet,
popover and tooltip are the platform's own <dialog> and popover
APIs; the accordion is <details>; tabs, the carousel's dots and
arrows, the segmented control and the whole of forms are CSS. Sorting a data grid,
picking a date on the custom calendar, encoding a QR code and firing a toast are the
things that genuinely need script.
So the honest test for a page is whether the scripts are doing work the platform
cannot. If deleting deck-extras.js changes nothing on a page, do not
ship it.