A CSS framework you add with one link tag.
Deck is a single stylesheet: buttons, forms, tables, a data grid, charts,
overlays, an icon sprite, and a full color system. There is no build step, no
config file, no purge pass, and nothing to install. It ships in cascade layers,
so your own CSS wins without !important, and every color on the page
derives from one number you can change at runtime.
Those eleven steps, every button, link, focus ring,
badge, chart series, and shadow on this page are computed from
--hue-brand. Drag the slider in the header and watch all of it
retune, live, with no rebuild.
What is Deck?
Deck is a CSS framework that ships as a single
26.8 KB stylesheet containing buttons, forms, tables,
a data grid, charts, overlays, an icon sprite, and a complete color system. You add
it to a page with one <link> tag, and it has no build step, no
configuration file, and 0 runtime dependencies.
Deck is written as 26 plain CSS
files that are concatenated into deck.css, defining
927 classes. It is published under the
MIT license, and version
0.1.3 is the release documented on this page.
What does Deck actually weigh?
A page that loads the stylesheet, the icon sprite, and the optional JavaScript
transfers 62.8 KB Brotli, or 76.3 KB gzip.
Every browser Deck supports sends br in Accept-Encoding, and
Cloudflare, Vercel, Netlify, and nginx with ngx_brotli negotiate it for
text by default, so Brotli is what most users receive.
| File | Brotli | gzip | What it is |
|---|---|---|---|
deck.min.css |
26.8 KB | 32.7 KB | The whole framework. Fixed size. |
deck-icons.svg |
27.1 KB | 33.6 KB | 75 icons at two weights, 152 symbols. Only if you use the icons. |
deck.min.js |
8.9 KB | 10.0 KB | Optional. Only for components that need behaviour. |
| All three | 62.8 KB | 76.3 KB | The honest total for a page that uses everything. |
The sprite is the largest single file, slightly bigger than the
stylesheet, which is worth saying plainly rather than leaving for you to find in
devtools. Swapping deck.min.js for the full
deck.bundle.min.js, which adds the date picker, combobox, data grid,
toasts, and QR encoder, makes the JavaScript 17.3 KB and the
total 71.2 KB Brotli
(85.8 KB gzip). Every figure here is measured by
npm run build; the page reads them from dist/sizes.json
rather than carrying its own copy.
The sprite is a manifest, not a fixed cost
The 75 icons in the sprite are a default so the demo works
out of the box, not a floor you have to pay. tools/icons/icons.txt is a
plain list of the icons to extract: delete the lines you do not need, run
npm run icons, and the sprite is rebuilt with only what is left.
# keep only the icons you use
$ cat > tools/icons/icons.txt <<'EOF'
check = check
search = search
settings = settings
@hand deck-mark
@hand deck-wordmark
EOF
$ npm run icons
A twelve icon sprite measures 5.7 KB Brotli (6.9 KB gzip) — that figure was generated and measured, not estimated. Against 27.1 KB for the full set, trimming the manifest to what a project actually uses is the difference between the sprite dominating the page weight and disappearing into it.
This matters because an external sprite is all or nothing per
request: the browser fetches the whole file to resolve one
<use>, so an unused icon is not free the way an unused CSS class
is. That is the reason the manifest exists. The two @hand lines carry
the brand marks through from the previous sprite; drop them and the marks are
dropped too.
Do I need a build step to use Deck?
No — Deck needs no build step, because it is distributed as a finished stylesheet with no compiler, no bundler plugin, no PostCSS pipeline, and no purge pass between you and a styled page. The file you download is the file the browser reads, which means a Deck project has nothing to rebuild when you change a color, and nothing to reinstall when you clone it onto a new machine.
How do I install Deck?
Add one <link> tag pointing at deck.css and the
framework is installed. The JavaScript file is optional and only needed for
components that require behaviour, such as the date picker, the combobox, and the
toast queue; every other component works as pure CSS with the script absent.
<link rel="stylesheet" href="/assets/deck/deck.css">
<script src="/assets/deck/deck.js" defer></script> <!-- optional -->
Deck is also on npm as @echodial/deck and on
Packagist as echodial/deck, and the
npx @echodial/deck init public/assets/deck command copies the files
into a project without installing anything permanently.
How do I change the color scheme?
Set --hue-brand to a number between 0 and 360 and every brand color
in the framework is recomputed from it. Deck derives its whole palette from six hue
values in oklch, so a theme change is one custom property
rather than a rebuild, a second stylesheet, or a set of overrides.
:root { --hue-brand: 265; } /* violet instead of teal */
<html style="--hue-brand: 320"> /* or per tenant, at runtime */
What browsers does Deck support?
Deck targets Chrome 117, Edge 117, Safari 17.4, and Firefox 128 and newer, which
are the versions that shipped cascade layers, container queries, oklch(),
and the popover attribute. Those features are load-bearing rather than
progressive enhancements, so Deck does not attempt to support browsers released
before them.
Is Deck free to use commercially?
Yes — Deck is MIT licensed, which permits commercial use, modification, and redistribution provided the copyright notice is kept. The bundled icon outlines are derived from Google's Material Symbols and carry the Apache License 2.0, whose notice is reproduced in the sprite's header comment.
How is Deck different from Tailwind CSS and Bootstrap?
Deck differs from Tailwind CSS and Bootstrap in that it has no build step and no configuration file, and its entire palette can be rethemed at runtime by changing one CSS custom property. Tailwind generates a stylesheet from your markup at build time, and Bootstrap needs a Sass compile to customise beyond its CSS variables; Deck is a fixed file that you link and then override with ordinary CSS.
| Question | Deck | Tailwind CSS | Bootstrap |
|---|---|---|---|
| Build step | None | Required — the stylesheet is generated from your markup | Not for the prebuilt CSS; required to customise via Sass |
| Config file | None | tailwind.config.js, or a CSS @theme block in v4 |
Sass variables |
| Runtime dependencies | 0 | 0 in the output CSS; a Node toolchain to produce it | 0 for the CSS; Popper for dropdown and tooltip JavaScript |
| CSS size, compressed | 26.8 KB Brotli, fixed | Varies with how many utilities you use | Fixed; see their release notes for the current figure |
| Retheme without rebuilding | Yes — one custom property, at runtime | Partly — v4 exposes CSS variables; config changes need a rebuild | Partly — v5.3 exposes CSS variables; Sass changes need a recompile |
| Right-to-left | Built in — set dir="rtl", no second file |
Logical-property utilities; a plugin for full coverage | A separate right-to-left stylesheet build |
| Overriding the framework | Cascade layers — unlayered CSS always wins | Utility order and !important where needed |
Specificity, or !important |
| Ecosystem and plugins | 19 first-party templates; no plugins | Large — component kits, plugins, templates | Large — themes, plugins, long-standing community |
| Editor tooling | Deck IntelliSense for VS Code and editors built on it | Official IntelliSense extension | Community extensions and snippets |
| Maturity | Version 0.1.3, one author | Established, funded, large team | Established since 2011, large team |
Where Deck loses
Deck has no plugin marketplace, no third-party component kits, editor support only in Visual Studio Code and the editors built on it, and one maintainer, so a team that needs a large hiring pool or a deep catalogue of ready-made themes is better served by Tailwind CSS or Bootstrap. Deck also has a fixed stylesheet size: a page that uses six components downloads the same 26.8 KB as a page that uses all of them, whereas Tailwind's generated output scales down with usage.
Deck's figures above are measured from this repository with
npm run build. The Tailwind CSS and Bootstrap columns describe
documented behaviour rather than measurements taken here; both projects change
between releases, so check their current documentation before relying on a number.
Buttons
Seven variants, three sizes, groups, and a loading state.
Cards and stats
Open support tickets
17 waiting on a reply
Opened eleven pull requests this week. Two are blocked on a review from the platform team.
The API will be read only on Sunday from 02:00 to 04:00 UTC.
Build 4471 is live on production. 42 seconds, no errors.
You are using 47 GB of your 50 GB plan.
The card ending 4242 was declined. Update it to keep the plan active.
Forms
Inputs are 16px on touch devices, so iOS never zooms when a field takes focus. Every control clears a 44px target.
Below 640px this table restacks into labelled rows instead of forcing a sideways scroll. Resize the window to see it.
Navigation and disclosure
How do I invite someone to a workspace?
Can I change my plan mid-cycle?
Where do I find my API key?
Icons and emoji
Deck ships 75 icons as a single SVG
sprite of 152 symbols — each icon at two weights, plus two
brand marks, 27.1 KB Brotli — and they
inherit color and font size, so they sit on the text baseline without nudging. The
sprite is generated from the Material Symbols variable font, so any of its
4,025 icons can be added by putting its name in
tools/icons/icons.txt and running npm run icons; only
the listed names are extracted, which is how a 4,025 icon
library ships as an 75 icon file. Emoji get a pinned font
stack so they render the same on Windows, iOS, and Android.
Overlays
Built on native <dialog> and the popover
attribute, so focus trapping, escape-to-close, and the top layer are handled by
the browser. The sheet slides up from the bottom on a phone and centers on a
desktop.
Clear a filter or widen the date range to see more.
Date picker
Single date, range with two months, and presets. It's a popover on a desktop and slides up as a sheet under 480px.
Combobox
Type to filter, arrows to move, enter to pick. The real
<select> stays in the DOM and stays in sync, so a plain PHP
form post works with nothing extra on the server.
Data grid
Frozen header, pinned first and last columns, sortable headers, resizable columns, and a totals row that sticks to the bottom. Scroll the grid sideways — the shadow only appears once you do.
| Order | Customer ID | Customer | Product | Rep | Status | Subtotal | Tax | Total | Placed | ||
|---|---|---|---|---|---|---|---|---|---|---|---|
| #1042 | cus_Q4nR8vTk2LpZ | Northwind Traders | Standard plan, 24 seats | Marco Reyes | Paid | $780.00 | $62.16 | $842.16 | 2026-09-03 | ||
| #1041 | cus_H7mE3bXw9Ktf | Globex Corp | Team plan, 8 seats | Ken Spence | Pending | $295.00 | $23.00 | $318.00 | 2026-08-31 | ||
| #1039 | cus_K2pA9zLd4Rye | Initech | Starter plan, 3 seats | Dana Whitfield | Refunded | $0.00 | $0.00 | $0.00 | 2026-08-27 | ||
| #1038 | cus_R5tY6wQn1Bge | Umbrella Ltd | Enterprise add-on, SSO | Priya Lakhani | Paid | $298.00 | $23.40 | $321.40 | 2026-09-05 | ||
| #1037 | cus_B8xC4uMs7Wdq | Vandelay Industries | Team plan, 6 seats | Tina Okafor | Pending | $278.75 | $22.00 | $300.75 | 2026-09-06 | ||
| 5 orders | $1,651.75 | $130.56 | $1,782.31 | ||||||||
Toast queue
Toasts stack instead of stringing down the screen. Hover the stack to fan it out and pause every timer. Swipe or drag one sideways to dismiss it. Fire several in a row to see the queue work.
Charts
No charting library. Bars and donuts are CSS driven by a
--value property; lines are inline SVG you style with classes.
Every series reads the brand palette, so the hue slider retunes these too.
Transitions and animations
Motion answers an action or shows what changed. Nothing here runs unless you ask for it by class, and everything is off for anyone whose OS asks for reduced motion — except loading indicators, which keep turning slowly, because a frozen spinner reads as broken.
These use animation-timeline: view() —
tied to scroll position with no IntersectionObserver. deck.js falls back to an
observer where the browser doesn't support it.
A real height: auto transition using
interpolate-size. No measuring in JavaScript, no max-height guess.
Add @view-transition { navigation: auto; } to
your app CSS and full page loads cross-fade like a single page app — no
router, no JavaScript, no framework. Deck styles what the browser generates,
including holding the header and tab bar still while the content changes.
Cascade layers
Deck declares the whole cascade contract in
00-layers.css before a single rule exists. Order is decided there —
not by file order, not by specificity, and never by !important.
Four empty app.* layers are reserved for you.
@layer
deck.reset, deck.tokens, deck.type, deck.layout,
deck.components, deck.mobile, deck.motion, deck.effects,
deck.utilities, deck.rtl, deck.print,
app.base, app.components, app.pages, app.overrides;
A rule in app.pages beats every Deck
rule with a single class selector. No specificity war, no stacked
.page .card .btn chains.
Anything outside a layer beats all layers. A one-off rule in a page template overrides Deck with nothing special.
Wrap a vendor stylesheet in its own layer with
@import url(x.css) layer(vendor) and it stops fighting you.
Container queries
The same card markup, twice. The one in the wide column goes horizontal; the one in the rail stays stacked. Neither knows where it was placed — they respond to the space they were given, not the window.
Designing with container queries
Guides · 8 min read · updated Mar 3
--tone on the container, children follow
Logical properties
Deck is written in logical properties end to end, so a full
right-to-left flip needs nothing but dir="rtl". Try the switch — the
layout, the switch knob, the chart fills, the sidebar, and the pointing icons all
mirror. The commit hash does not, because an identifier reads left to right in
every language.
Identifiers stay in their own direction and stay isolated from the text around them.
Gradients
All built from the brand hue and interpolated in oklab, which avoids the grey dead zone you get blending two saturated colors in sRGB. Drag the hue slider and every one of these retunes.
Ship it on Friday
Mesh background, gradient text, no images
Two clip boxes, works with any radius.
The angle is a registered @property, so it interpolates.
Clipped to the glyphs, with a real color underneath.
3D transforms
Depth earns its place when it carries meaning: a card with two
sides, a pile you are working down through, a control that physically depresses.
Everything uses rotate and translate as individual
properties, so two effects compose instead of overwriting each other.
Northwind Traders · 24 seats
Subtotal $780.00 · Tax $62.16
Move the pointer across this card. The title floats above the face on the Z axis.
Carousel, drawer, mega menu, speed dial
The carousel is scroll snap underneath, so it swipes correctly with JavaScript off. Arrows and dots are enhancement.
Saved views, a public API for boards, and faster search.
sk_live_7Kd2Rq9XmB4tPw
Everything you need on the first page load
One stylesheet, one link tag, and no dependencies. Every component on this page came out of the box.
Gallery
Equal tiles for a media library — screenshots, exports, brand
assets. .gallery is a grid of square cells that reflows on its own,
so someone uploading nine files and someone uploading three both get a tidy
block. .span-2 promotes a tile to a 2×2 feature and
.span-wide makes it a 2×1 banner, which is how you lead with
the one image that should be seen first.
Sidebar
.sidebar is a bare nav list — .sidebar-group
for a heading, .sidebar-link for a row, .push to shove a
count to the far end. It brings no width and no chrome of its own, so it goes in
whatever rail your shell already has. Give that rail .cq-shell and the
links collapse to icons below 15rem — a container query, not a media query, so it
keys off the rail and not the window. Drag the rail by its bottom
corner and watch the labels drop out while the page width never
changes.
Open issues
Forty-two issues are waiting on something. Thirty-one of them are waiting on a reviewer, not on the build.
The rail beside this column is the same markup at every width. Nothing inside the sidebar knows how wide it is — the container does.
Tooltips
There are two of them and the difference matters.
.tooltip is a ::after on the trigger with the text in
data-tip — no extra markup, no JavaScript, nothing to keep in sync.
But it is pinned above the trigger and cannot flip, so near the top of a
scrollport it runs off the edge, and it hides itself on coarse pointers because a
hover tip never worked on a phone anyway. .tip is a real popover
placed with CSS anchor positioning, so it can flip, and it carries an
arrow that stays pointed at its anchor. Reach for .tooltip for a
short label on an icon button in the middle of a page; reach for .tip
when the text is longer, has to survive an edge, or should open on click.
Optional libraries
Deck's core stays zero-dependency. deck-adapters.js
detects a library if you've loaded it and hands that job over, keeping Deck's
markup and styling unchanged. Load none and everything still works.
| Job | Deck alone | With a library | Verdict |
|---|---|---|---|
| Placement | CSS anchor positioning | Floating UI, 9 KB | Library only where anchor positioning is missing |
| Rich text | execCommand, deprecated |
Tiptap or Quill | Use the library |
| Charts | CSS charts that retheme | Chart.js, themed by Deck | CSS for tiles, Chart.js for real axes |
| Drag and drop | Native HTML DnD, poor on touch | SortableJS | Use the library |
| Icons | 75-icon sprite, trimmable to what you use | Lucide, 1500 icons | Sprite is enough for Deck; Lucide for the rest |
| Long lists | content-visibility |
A virtualizer, 5 KB | Keep the browser; it doesn't break find-in-page |
| Dates and locales | Intl, built in |
date-fns or similar | Keep Intl |
Call Deck.adapters.report() in the console. It
names what is actually doing each job on this page, which is what you want when
a component behaves differently between two environments.
An ordering mechanism that sits above specificity. A rule in
a later layer beats one in an earlier layer no matter how the selectors compare,
which is how your CSS overrides Deck without !important.
One line of CSS. Off-screen rows are skipped during layout, style, and paint, but they stay in the DOM — so Ctrl+F still finds them and the print stylesheet still prints them, which a JavaScript virtualizer breaks. No library, no fixed row height, no scroll listener.
| Order | Customer ID | Customer | Rep | Status | Total |
|---|
Intl, not a date library
Invoices, packing slips, and reports still end up on paper. Print this page and the nav, tab bar, buttons, toasts, and theme dock drop out; the grid unfreezes and prints every column; the mobile card fallbacks revert to real tables; dark mode is forced back to light.
The block below is hidden on screen and appears in the print preview.
Invoice INV-2041 — Northwind Traders
Printed on 2026-09-07.