JavaScript API

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
<button class="btn btn-icon btn-ghost" data-deck-theme aria-label="Switch theme"> <svg class="icon"><use href="../../assets/deck/deck-icons.svg#moon"></use></svg> </button>
<button class="btn btn-icon btn-ghost" data-deck-theme aria-label="Switch theme">
  <svg class="icon"><use href="../../assets/deck/deck-icons.svg#moon"></use></svg>
</button>

Attributes Deck sets on itself

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
AttributeMeaning
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)
ParameterTypeMeaning
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)
ParameterTypeMeaning
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.
Deck.toasts.max = 5;
window.addEventListener('beforeunload', () => Deck.toasts.clear());

Defined in src/js/deck.js.

Deck.theme(mode?)

Reads or sets the colour theme by writing data-theme on the <html> element.

Parameters for Deck.theme(mode?)
ParameterTypeMeaning
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)
ParameterTypeMeaning
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?)
ParameterTypeMeaning
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?)
ParameterTypeMeaning
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.
const { months, weekStart, long } = Deck.locale('fr-FR');
months[0];              // 'janvier'
weekStart;              // 1
long.format(new Date());

Defined in src/js/deck.js.

Deck.transition(update, opts = {})

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 = {})
ParameterTypeMeaning
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.
Deck.transition(() => row.remove());

await Deck.transition(() => {
  list.prepend(newRow);
}, { direction: 'forward' }).finished;
newRow.focus();

Defined in src/js/deck.js.

Deck.play(node, className)

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)
ParameterTypeMeaning
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.
await Deck.play(field, 'shake');
field.focus();

Deck.play(card, 'flash-good');

Defined in src/js/deck.js.

Deck.toggle(node, force?)

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?)
ParameterTypeMeaning
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.
trigger.addEventListener('click', () => {
  const open = Deck.toggle(panel);
  trigger.setAttribute('aria-expanded', String(open));
});

Defined in src/js/deck.js.

Deck.flip(node, force?)

Toggles .is-flipped, which turns a flip card over.

Parameters for Deck.flip(node, force?)
ParameterTypeMeaning
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)
ParameterTypeMeaning
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.
Deck.advance(document.querySelector('.stack-depth'));

Defined in src/js/deck.js.

Deck.face(cube, name)

Rotates a 3D cube to a named face by setting data-face.

Parameters for Deck.face(cube, name)
ParameterTypeMeaning
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)
ParameterTypeMeaning
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")
ParameterTypeMeaning
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.
node.innerHTML = Deck.qr.svg('https://example.com/ticket/1042', 'H');

Defined in src/js/deck-extras.js.

Deck.adapters

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.
Deck.adapters.available.chart;   // false
Deck.adapters.report();
// { placement: 'CSS anchor positioning',
//   editor: 'Deck built-in (execCommand)',
//   charts: 'Deck CSS charts', … }

Defined in src/js/deck-adapters.js.

Deck.chart(canvas, config)

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)
ParameterTypeMeaning
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
PropertyTypeNotes
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.
Deck.version string The version of the script, as a string.
Deck.toasts object The toast queue. See above.
Deck.chartTheme object | undefined { 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.