Repository navigation
This was going to be 0.1.4. One default below changes how existing pages look, and in 0.x a breaking change moves the minor version, so it is 0.2.0. A ^0.1 range or a CDN URL pinned to @0.1 stays on 0.1.3 until you move it to ^0.2 or @0.2.
Breaking
- Primary buttons are lighter in dark mode.
.btn-primarynow fills with--brandand--brand-hover(see An exact brand colour reaches.btn-primaryunder Fixed). In dark mode those are the 400 and 300 steps, so the button takes the lighter colour a link uses, with black text. Until now it was the 600 step with white text in both schemes. Light mode is unchanged. To keep the old dark look, add one line to your CSS:@layer app.overrides { .btn-primary { --brand: var(--brand-600); --brand-hover: var(--brand-700); --btn-text: var(--text-on-brand); } }. It matches 0.1.3 at rest and on hover, in both schemes. - The npm package no longer carries the docs build data.
dist/api.json,dist/api-buckets.json,dist/usage.jsonanddist/collisions.jsondescribe Deck to its documentation site and editor extension, and nothing in a page or innpx @echodial/deckreads them. The tarball is 2.3 MB unpacked instead of 4.3 MB. If a script of yours read them fromnode_modules, take them from the Composer package or the repository, which still have them. - Two fixes below also change existing markup, and each says what to do if you relied on the old behaviour:
.scroller's new padding (Focus rings inside lists and scrollers are whole and Scrolled cards rest at the gutter), and the on state a.btnnow has witharia-pressed="true"(Toggle buttons, under Added).
Fixed
- Modals open centred.
.modalnow setsmargin: auto. The reset's* { margin: 0 }removed the browser's own centring of modal dialogs, so every.modalopened withshowModal()sat in the top-left corner of the window. - Icons in running text stay on the line.
.iconis nowdisplay: inline-block. The reset'ssvg { display: block }put an icon inside a sentence on a line of its own, despite.icon's baseline alignment. Icons in flex and grid rows (buttons, alerts, tiles, fields) are laid out exactly as before, because flex and grid items are blocks either way. - Long modals keep their footer. An open
.modalis now a flex column, and so is a<form method="dialog">wrapped around its regions..modalcaps its height atmin(85dvh, 48rem)and clips, and.modal-bodywas meant to scroll, but an open modal was laid out as a block, so the body never shrank: in a window shorter than the dialog, everything past the cap was clipped, the footer and its buttons included. Now the body scrolls while the header and footer keep their height.flex-direction: columnis on.modalitself rather than on.modal[open], so the regions stay stacked while a closing modal fades out. - Dark mode no longer has light blocks in it.
.progress,.ring,.spinner,.skeleton,.rangeand.range-pairdrew their tracks with--ink-100and--ink-200, and the status badges, alerts and.banner-warnfilled with--good-100,--warn-100and--bad-100. All of these are fixed light values, so each drew a near-white shape on a dark page, and a.btn-ghostinside.alert-good,.alert-warnor.alert-badwas all but invisible (1.03:1). They now use theme-aware tokens. Light mode is unchanged, pixel for pixel. - The combobox placeholder is escaped.
data-placeholderwas written into the combobox's markup unescaped, so a placeholder containing a double quote could add attributes to the input, anonfocushandler included, and an ordinary placeholder with a quote in it was cut short. It is now escaped like every other string the combobox renders. The markup and data attributes are unchanged. .banner-closeclears the 44px touch target. It was only as big as its icon, 25×22px. It is now a--tapsquare with the icon at its old size, and it reaches into the banner's padding rather than making the strip taller. A.btn.banner-closeis unchanged, because.btnalready had a 44px target.- The combobox list opens next to its field. Where the browser has anchor positioning, the list covered the field it belonged to on every page, because
position-areawas measured inside.combo, which is only as tall as the control. On a phone inside a.card, the list was laid out against the card rather than the screen, since the card'scontain: layoutcaptures evenposition: fixed, and the card'soverflow: clipthen cut it off. deck.js now opens the list as a popover in the top layer, so no ancestor can clip it, and places it with anchor positioning, the Floating UI adapter, or its own placement, in that order. On a phone it stays anchored to the field, with 44px rows, instead of becoming a sheet across the bottom of the screen. - Remote comboboxes keep earlier choices, and post them. With
data-urlanddata-multi, a choice made under one query was dropped by the next unless the new results happened to contain it, and no remote choice ever reached the<select>, so the form posted none of them. The selection is now kept apart from the results, every chosen value is written into the<select>, a token can be removed whatever the current results are, and a slow reply to an old query no longer replaces a newer one's results. - Long comboboxes open at once. Only the options near the scroll position are drawn, so 10,000 options open in about 16ms instead of 1.4s (Edge). The arrow keys still reach every option and wrap at the ends,
PageUpandPageDownmove a screenful, and the active option is announced througharia-activedescendantwith its place in the whole list. The docs had always saidaria-activedescendantwas set; it was not.ArrowUpwith nothing active now goes to the last option rather than the second-to-last. - Focus rings inside lists and scrollers are whole.
.listclips, so a focused.list-rowlost the sides of its ring, plus the top on the first row and the bottom on the last. The ring is now drawn inside the row, and follows the list's rounded corners..scrollerclipped the top of every focused item's ring and faded the ring of an item at the end edge. It now has 4px of top padding, so content below a scroller moves down 4px. Its fade lifts while keyboard focus is inside. - Scrolled cards rest at the gutter.
.scrollerhasscroll-padding-inlineequal to its own inline padding. Before, the first card snapped flush against the screen edge on load, and so did any card scrolled into view. A scroller you have made flush withpadding-inline: 0needsscroll-padding-inline: 0as well. .card-linkresponds to keyboard focus. Tabbing to the stretched link, or to anything else in the card, gives the card the same lift as hover..nav-linkkeeps one height with or without an icon. It is nowinline-flexwith centred items, so an icon no longer sits on the text baseline, where its size and the font could make the link taller. Text-only links are the same size as before.- A
.btnlabel split at a bare<span>reads as one phrase.Save <span>changes</span>used to put the 8px gap between the words. A span with no class and no child elements now joins the label's text. A span with a class, such as a badge or.sr-only, keeps its box and the gap. .form-actions-stickysits above.tabbar. It used to slide underneath the tab bar.body:has(.tabbar)now publishes its clearance as--tabbar-clearance, the sticky bar uses it, and the bar drops its own safe-area padding while the tab bar is showing.aria-pressedandaria-selectedboth mean "on"..chip,.reactionand.datepicker-presetused to respond only toaria-pressed="true", and.segmentedonly toaria-selected="true", so the wrong one showed no state and raised no error. All four now accept both, with the same look as before. deck.js moves either attribute when a.segmentedis clicked, and no longer selects anaria-selectedtable row that contains one.- Small pointing icons mirror under
dir="rtl". The automatic mirror insrc/19-logical.cssmatched#chevron-rightbut not#chevron-right-sm, so an.icon-smarrow or chevron kept pointing the wrong way in a right-to-left page. Every pointing icon is now listed with its-smcut. .g-scrimdarkens an<img>child. The scrim painted under every child, so with the photo as an<img>, rather than as the element's background, the caption sat on the bare image. Animg,picture,video,canvas,svg,iframe,.lazyor.videochild now sits under the scrim; other children still sit above it.- A failed
.lazyimage shows that it failed. deck-extras.js set.is-erroron the frame and nothing styled it, so the shimmer ran forever. The shimmer now stops and a broken-image glyph appears in the frame. The script also catches an image whose markupsrcfailed before the script loaded, which fired no event. .grid-fixed-2,-3and-4work without.grid. They set only the columns, so on their own they did nothing. Each now setsdisplay: gridand the gap itself;grid grid-fixed-3is unchanged, and print unwinds them like.grid..split-rail-startis described correctly. It sizes the first track as the rail, so the rail goes first in the markup. Its comment and the split docs said the opposite, and the docs example showed the main column squeezed into the rail's width..filetakes a dropped file. It looked like a drop zone and ignored drops. With deck-extras.js, dragging a file over it sets.is-over(a solid border and the focus ring), and a dropped file goes into the input as a pick would: filtered byaccept, one file unlessmultiple, theninputandchange. A drop the page's own handler has already taken is left alone.- SortableJS can load after Deck. With Deck deferred, a SortableJS deferred after it or loaded
asyncwas not there when the adapters first ran, and the list was left on the native fallback for good, so drag handles did not work on first load. A list on the fallback is now handed to SortableJS when it arrives (atDOMContentLoadedorload, or on any laterDeck.init()). The native fallback now honoursdata-handle. - Small icons from Deck's helpers use the small cut.
Deck::icon($name, 'icon icon-sm')and the icons deck.js renders with.icon-sm(date picker, combobox, toasts) pointed at the regular#namesymbol rather than the heavier#name-smdrawn for 16px. .video-postershows its play button. The poster image and the button were two grid rows, so the image overflowed the frame and the button sat below it, clipped. The image now fills the frame and the button is centred on it.- An exact brand colour reaches
.btn-primary. The documented override,--brandand--brand-hoveron:root, recoloured links and outline buttons but not primary buttons, which filled with the--brand-600and--brand-700ramp steps. They now fill with--brandand--brand-hover. Light mode looks exactly as before. In dark mode a primary button is now the lighter step a link uses, with black text worked out from the fill, where it used to be the 600 step with white text in both schemes. - A dark section on a light page has readable text.
data-themeon an element other than<html>switched its tokens but not its text, which<body>had already worked out, so adata-theme="dark"section kept dark text on its own dark cards (1.14:1). The element now takescolor: var(--text), at zero specificity, so text on its surfaces is 16.96:1. A colour your component sets still wins. - A chart's series 1 follows the chart's hue.
--c1wasvar(--brand-500), worked out on the root, so a.chartwith its own--hue-brandhad series 2 to 6 in the new hue and series 1 in the page's.--c1is now the sameoklch()recipe written out, so all six follow. At the root's hue it is the same colour as before. - Blurred chrome respects reduced transparency. Under
prefers-reduced-transparency: reduce,.sticky-top,.form-actions-sticky,.tabbar, the carousel arrows (Deck's own and the native::scroll-buttons) and.video-playnow drop their blur and become their opaque--bgor--surface, as.glassalready did..modal::backdropbecomes solid, since without the blur the page would show through it sharp. A.sticky-topstill in flow stays see-through. Without the setting nothing changes, pixel for pixel.
Added
- Read, set and clear a combobox from script.
el._deck.value()returns the selection (a string, or an array withdata-multi),el._deck.value(v)replaces it with a value, an array, or{ value, label }objects, andel._deck.clear()empties it. Both keep the<select>in step and firechangeanddeck:changeas a pick does;{ silent: true }skips the events. - Toggle buttons. A
.btnwitharia-pressed="true"now has a visible on state: the brand tint with a brand edge. A pressed.btn-softfills solid, and the solid variants go a shade darker.aria-pressedis now Deck's attribute for "on"; the table atdocs/components/button.php#on-attributelists what each component accepts. Pages that already putaria-pressedon a.btnwith their own pressed styles will see Deck's on state wherever their rules leave a property unset. - Seven theme-aware tokens.
--surface-recessedis the recessed surface a control sits on: a progress, range or ring track, or a skeleton's sweep.--good-soft,--warn-softand--bad-soft, each with a-textpartner, are the status equivalents of--brand-softand--brand-soft-text. Reach for these rather than the--ink-*and status ramps, which are the same colour in both themes. - Layout regression tests that render the built stylesheet in a real browser:
npm test. They use Microsoft Edge by default; setDECK_TEST_BROWSERtochromeor to the path of a Chromium executable to use another.playwright-coreis a dev dependency only; Deck still ships no runtime dependencies. - Ten icons.
inboxandarrow-down-right, plus a media set:play,pause,skip,fullscreen,tv,film,musicandbroadcast, each in both cuts. The sprite is 86 icons, 174 symbols.docs/components/icon.php#setlists every icon in it. - Layout utilities.
.ratio-2x3for posters and book covers,.object-containto letterbox instead of crop (it beats a.ratio-*frame's cover),.break-words(overflow-wrap: anywhere, which also stops a long string widening a flex item or table cell), and.z-sticky,.z-navand.z-sheet, which put your own chrome on Deck's named stacking layers. .input-smand.input-lg, the heights of.btn-sm(34px) and.btn-lg(50px). They work on.selectand inside.searchtoo. Every control is still 16px on a touch screen..list-flush, a list with no border, radius or fill of its own, for a list inside a card or a sheet.- Dismissible alerts. A
<button class="alert-close">last in an.alertcloses it: a 44px target that does not make the alert taller. deck.js hides the alert after a cancellabledeck:dismissevent, and works for alerts inserted after the page loaded. .grid-fill, the auto-fit grid withauto-fill, so a short last row keeps card-sized columns instead of stretching. It needs no.gridbeside it..glass, a frosted, theme-aware surface for a card, panel or bar over an image:--surfaceat 72%, the backdrop blurred and saturated, a see-through border and a top-edge highlight that is weaker in dark mode. It sets no radius, border width or shadow, so a.cardkeeps its own. Three custom properties tune it,--glass-blur(16px),--glass-tint(72%) and--glass-edge(22%), and three classes set one each, so they combine:.glass-smand.glass-lg(8px and 32px blur) and.glass-solid(90% tint, for muted text over a photo). Wherebackdrop-filteris not supported, and underprefers-reduced-transparency: reduce, it is the opaque surface with the component's own border..g-scrim-autoand--scrim-auto, a scrim that follows the theme, with--textover it: a light floor in a light theme, and exactly.g-scrimin a dark one, so switching to it changes nothing in dark mode..g-scrimstays dark in both, for white text.--scrim-strength, a multiplier on the scrim's opacity for.g-scrim,.g-scrim-autoand either with.g-scrim-top. The default is 1. Use less for a flat illustration, and about 1.3 under muted text, which falls below 4.5:1 over some photos at the default. While it is unset, each class paints its token, so a retuned--scrimstill applies.Deck::head(),Deck::css()andDeck::js()take a CSP nonce.Deck::head(nonce: $nonce)puts it on the stylesheet link, every script tag and the hue<style>, so the helper works under a strict Content Security Policy.- One region, its own brand:
data-theme-scope.--hue-brandor--chroma-brandon an element now rethemes it when the element also hasdata-theme-scope. Deck declares its 18 brand tokens on:rootand on[data-theme-scope], so a marked element works them out from its own hue: buttons, links, badges, focus rings, tints and chart series all follow, and scopes nest. Before, a hue below the root reached only gradients and five chart series, and the theming guide said so. Without the attribute that is still all it reaches, so a.g-meshwith an inline hue behaves as before. A--brandoverride on:rootstops at a scope; set it on the scope to reach inside.Deck::theme(…, scope: true)emits the attribute beside the custom properties; withoutscopeits output is unchanged.Deck.hue()on the root paints exactly as before. - Blur tokens.
--blur-xs(3px, the modal backdrop),--blur-sm(8px, carousel arrows and.video-play),--blur-md(12px,.sticky-topand.form-actions-sticky) and--blur-lg(16px,.tabbar). Every blurred surface reads one, so retuning a strength is one change on:root. The values are the ones those surfaces already used, and eachsaturate()stays as it was..glasskeeps its own--glass-blur. - The docs can be searched for a component by what it is called.
docs/components/index.phplists every public class under the page that documents it, generated fromdist/api.jsonand the pages on each request, with the classes no page covers yet at the end.?format=jsonreturns the same index for a script or an AI assistant. The docs search now ranks pages by title, heading, description and the classes they claim, and treats "date picker", "datepicker" and.datepickeras one search. From the navbar it opens the page when one is clearly the answer, at the heading for the class:donutopens Charts at Donut. The old search showed a file and line for each class and never linked a page. "date picker" missed.datepicker, "upload" found nothing, and "ring" listed.alert-bodyfirst. llms.txt now points assistants at the index rather than at README's hand-kept component list. - Deck IntelliSense, an editor extension. Completion for classes, tokens and icon names, hover docs, and diagnostics for a class that is off a scale (
.stack-5), removed, internal or one character from a real one, in Visual Studio Code, Cursor and Antigravity. Everything it knows is generated from the release it is built against. It is not on a marketplace: installdeck-intellisense-0.1.1.vsixfrom the GitHub release or fromtools/vscode/.docs/start/editor.phpcovers setup, every setting and troubleshooting. - Nineteen page templates in
public_html/templates/, with a gallery page: a store, agency and local business sites, an app shell, settings, checkout, pricing, sign-in, and galleries of footers, sidebars, heroes and carousels. Each is a single HTML file with no build step. One is unfinished:dashboard.htmlhas its top bar, sidebar and phone drawer, but its main area holds only the page title while it is rebuilt.
Changed
src/deck-icons.svggainedmenu-bookandmenu-book-smafter 0.1.3. The brand artwork, which uses only thedeck-marksymbol, did not change;src/brand/sources.jsonrecords the sprite's new hash so the build accepts it.menu-bookis now listed intools/icons/icons.txtas well, so regenerating the sprite keeps it.Deck::head()no longer prints an inline<script>to set the sprite URL.Deck::js()puts it on the first script tag asdata-deck-icons, which deck.js already read; the result is the same.Deck.versionis the whole version,'0.2.0'. From 0.1.0 to 0.1.4 it said'0.1', and so did the banners ofdeck.js,deck-extras.jsanddeck-adapters.js, because they are written by hand. The build now refuses to run when any of them, orDeck::VERSION, differs frompackage.json.- The Composer branch alias for
dev-mainis0.2.x-dev.