Move a page from Tailwind
You have a Tailwind project and you are wondering what a page looks like in Deck. This is that, in markup rather than adjectives — and then an honest list of what you give up, because there is a real list and you will find it on day three whether or not it is on this page.
The one difference everything else follows from
Tailwind gives you utilities and expects you to build the components. Deck gives you components and a smaller set of utilities for the gaps. That is the whole thing, and every trade below is a consequence of it.
Tailwind's own documentation is explicit about this — if you need to reuse styles, it
tells you to make a component in your template language or your framework. That is
correct advice for a utility framework, and it means the button in your project is a
button you designed, maintain and debug. In Deck the button is
.btn, and its focus ring, disabled state, loading state, icon alignment,
44px touch target and print appearance are somebody else's problem.
So: if your product's value is in a distinctive interface, Tailwind's flexibility is the point and you should keep it. If your product's value is somewhere else and the interface needs to be good rather than novel, that flexibility is a bill you pay every sprint.
The same things, both ways
A card
Tailwind
<div class="rounded-lg border border-gray-200 bg-white p-6 shadow-sm
dark:border-gray-700 dark:bg-gray-800">
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">
Invoice INV-2291
</h3>
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
Northwind Traders
</p>
</div>
Deck
<div class="card">
<div class="card-body">
<h3 class="card-title">Invoice INV-2291</h3>
<p class="text-sm text-muted">Northwind Traders</p>
</div>
</div>
Twelve utility classes against four component classes. The interesting part is not
the count — it is that the Tailwind version names gray-200,
gray-700, gray-800, gray-900,
gray-400 and white, so six colour decisions are now in
this file and will need finding again if the palette moves.
A button
Tailwind
<button class="inline-flex items-center gap-2 rounded-md bg-indigo-600
px-4 py-2 text-sm font-semibold text-white shadow-sm
hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2
focus-visible:outline-offset-2 focus-visible:outline-indigo-600
disabled:opacity-50 disabled:cursor-not-allowed">
Save changes
</button>
Deck
<button class="btn btn-primary">Save changes</button>
The hover, focus and disabled states are in the Tailwind markup because they have to be somewhere. Deck puts them in the class. Both are legitimate; the difference shows up when the focus ring needs to change and there are 340 buttons.
A form field
Tailwind
<div>
<label for="email"
class="block text-sm font-medium text-gray-900 dark:text-white">
Email
</label>
<input type="email" id="email" required
class="mt-2 block w-full rounded-md border-0 py-1.5 px-3
text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300
focus:ring-2 focus:ring-inset focus:ring-indigo-600
dark:bg-white/5 dark:text-white dark:ring-white/10">
<p class="mt-2 text-sm text-gray-500">Invoices go here.</p>
</div>
Deck
<div class="field">
<label class="label" for="email">Email</label>
<input class="input" id="email" type="email" required>
<p class="help">Invoices go here.</p>
</div>
Deck also styles :user-invalid here, so the field turns red after
someone has interacted with it and left it wrong — not on page load, which is what
:invalid does. Getting that behaviour in Tailwind means a variant or a
little JavaScript. See the forms guide.
Dark mode
Tailwind
<div class="bg-white text-gray-900
dark:bg-gray-900 dark:text-gray-100">
<!-- and dark: on every coloured element below -->
</div>
Deck
<div class="bg-surface">
<!-- nothing else. The tokens hold both values -->
</div>
Tailwind's dark: variant is per-utility, so a dark mode is a second
colour decision on every element that has a first one. Deck resolves it in the token
with light-dark(), so there is one declaration. Deck's way is less
flexible — you cannot make one card invert independently — and for the common case of
"the whole app has a dark mode" it is far less markup. Both approaches have three
states worth supporting: light, dark, and following the system.
Changing the brand colour
Tailwind
/* app.css */
@import "tailwindcss";
@theme {
--color-brand-500: oklch(0.62 0.14 265);
--color-brand-600: oklch(0.52 0.14 265);
/* …and the other nine steps */
}
/* then rebuild, and update every bg-indigo-600 in the markup */
Deck
<html style="--hue-brand: 265">
This is the widest gap between the two. Tailwind v4 moved theming into CSS with
@theme, which is a real improvement over the old JavaScript config — but
you still supply the ramp and you still rebuild. Deck generates eleven OKLCH steps
from one hue angle at runtime, which is what makes per-tenant theming a database
column rather than a build. See the theming guide.
Right to left
Tailwind
<!-- use the logical utilities throughout -->
<div class="ms-4 me-2 ps-3 text-start">…</div>
<!-- and audit for every ml-, mr-, pl-, pr-, text-left -->
Deck
<html dir="rtl">
<!-- Deck has no physical utilities to audit for -->
Tailwind has the logical utilities — ms-, me-,
ps-, pe-, start-, end- — and they
work well. The difference is that it also has ml- and
mr-, so an RTL migration is an audit. Deck ships no physical spacing
utility at all, which removes the audit by removing the option.
A translation table
Two things transfer directly, which makes the first hour easier than you expect.
Tailwind's default spacing unit is 0.25rem, so p-4 is
1rem. Deck's --space-4 is also 1rem, so .p-4 is the same
1rem. And Tailwind's sm, md and lg are 40rem,
48rem and 64rem — which are exactly Deck's three breakpoints. Your muscle memory
for both is correct.
| Tailwind | Deck | Notes |
|---|---|---|
p-4, px-4, py-4 | .p-4, .px-4, .py-4 | Same value. Deck has only 0,1,2,3,4,6,8 |
ml-4, mr-4 | .mis-4, .mie-4 | Logical only. No physical version exists |
space-y-4 | .stack-4 | A real gap, not a sibling selector |
flex items-center gap-3 | .cluster | Also wraps by default |
grid grid-cols-3 gap-4 | .grid | Auto-fits from a min width; no column count |
text-sm, text-lg | .text-sm, .text-lg | Same names, different values |
font-semibold | .fw-semi | 620, not 600 — the face is variable |
text-gray-500 | .text-muted | A role, so it follows dark mode |
bg-white | .bg-surface | As above |
rounded-md, shadow-sm | .r-md, .shadow-1 | Five radius steps, five shadow steps |
md:flex, lg:hidden | md:flex, lg:hidden | Same widths. Deck's set is fixed, not generated |
sr-only | .sr-only | Identical |
p-[13px] | — | No arbitrary values. Write a rule in your layer |
What you give up
All of these are real, none of them are going away soon, and any one of them can be the reason not to switch.
- Editor support, outside one family of editors. This is the one people feel first. Tailwind ships an official IntelliSense extension for VS Code with completion, linting and hover previews of the generated CSS; Zed and the JetBrains IDEs have equivalent support built in; and there is an official Prettier plugin that sorts classes for you. Deck has Deck IntelliSense, which completes classes, tokens and icon names, shows what a class declares on hover, and underlines off-scale and misspelt classes as you type — but only in Visual Studio Code and the editors built on it, such as Cursor and Antigravity, and installed from a file rather than a marketplace. In JetBrains, Zed, Sublime Text or Neovim there is nothing, and there is no class sorter in any editor. Outside VS Code you will be reading the class reference in another tab for the first week.
-
Class names you cannot derive. Tailwind's are mechanical: knowing
p,tand the scale gets youpt-7without looking it up, and the same grammar generalises to properties you have never used. Deck has 931 classes, 828 of them public, and a good proportion are names rather than formulas —.check-note,.list-trail,.dg-cards-wrap,.speed-dial-label. You cannot guess those; you learn them or you look them up. It is a genuinely larger memory cost and the reference exists because of it. - The ecosystem. Tailwind has first-party plugins for typography and forms, a large community of component libraries, templates, UI kits and Stack Overflow answers, and any AI assistant you use has seen enormous amounts of Tailwind and almost no Deck. Deck has this documentation site and the source. If you get stuck at 2am, that difference is the whole difference.
-
Arbitrary values.
p-[13px],grid-cols-[24rem_1fr],bg-[#316ff6]— Tailwind's escape hatch for the one-off. Deck has no equivalent syntax. The answer is a rule in@layer app.pagesor astyleattribute, which is more keystrokes and puts the value somewhere other than where you are working. -
A fixed 26 KB, whether you use it or not. Tailwind generates only
the classes you actually used, so a small site ships a very small stylesheet. Deck's
deck.min.cssis 26 KB brotli — 169 KB raw — for a landing page with three buttons and for a whole application, because there is no build step and therefore nothing to purge. Above a certain app size Deck comes out ahead and it never grows; below it, Tailwind is smaller. -
A newer browser floor. Tailwind v4 targets Safari 16.4+, Chrome 111+
and Firefox 128+. Deck's package declares Chrome and Edge 117+, Safari 17.4+ and
Firefox 128+, and it leans on
light-dark(),@containerstyle queries and:has(). If you have to support Safari 16, Deck is not an option. - Maturity. Tailwind is at v4.3 with years of production use behind it. Deck is v0.1. It is on npm and Packagist and installs like any other package, but it has none of those years. Its public API is frozen and checked on every build, which is the right start, but it is a start.
What you get
-
No build step, at all. Not a faster one — none. No PostCSS, no Vite
plugin, no scan of your source files for class names, no watcher, no CI step that regenerates CSS. Tailwind's own
docs are clear that the Play CDN is "designed for development purposes only, and is
not intended for production", so a build is not optional there. Deck is a
<link>tag in any stack that can serve a file. - Components you did not have to build. A data grid, a date picker, a combobox, charts, toasts, a kanban board, a rich text editor and a QR encoder. In Tailwind each of those is a decision, a dependency, or a week.
- Retheming from one number. Covered above. If you sell to businesses who want their own colours, this is a feature you would otherwise build.
-
Markup you can read. A card is
.card. Six months later a diff on a template shows what changed rather than a reflowed wall of utilities. - Print, RTL and reduced motion, already done. 64 print rules, a logical-property codebase, and animations that are only written for people who have not asked for less motion. All three are things teams intend to do and do not.
Migrating in practice
Do not convert a codebase. Run both, convert at the page boundary, and stop when it stops paying.
- Load Deck first, then Tailwind. Tailwind's output is unlayered, and unlayered CSS beats every cascade layer — so Tailwind utilities will win over Deck components wherever both apply, which is what you want during a migration. If you want the reverse, import Tailwind into a layer that sits before Deck's.
-
Convert one page, not one component. Deck's value is in the
primitives composing, so a
.carddropped into a Tailwind flex column shows you the smallest part of the benefit and all of the friction. - Start with a form. It is where the ratio is best — the field, label, help, error and validation styling is a lot of Tailwind and very little Deck — and it is the fastest way to find out whether the trade suits you.
-
Delete the
dark:variants as you go. Once an element uses Deck's role tokens it has a dark mode, and the leftoverdark:classes will fight it. - Keep Tailwind for your bespoke screens. There is no prize for removing it. A marketing page with a custom layout is exactly what utilities are good at.
When not to switch
- Your interface is the product. A design tool, a game, anything with a distinctive visual language. Components will be in your way.
- You need Safari 16. Not negotiable.
- Your team is fast in Tailwind. Fluency is worth more than either framework's feature list, and you would be trading a known speed for an unknown one.
- You ship a small marketing site. Purged Tailwind will be smaller than 26 KB and you will not use the components.
- You depend on a Tailwind component library. Replacing a UI kit is a much bigger project than replacing a stylesheet.
What was checked, and what was not
Every claim about Tailwind here was read off tailwindcss.com at
v4.3 while this page was written: the build-step requirement and the
Play CDN's production warning, the browser floor, the sm/md/lg
widths, the 0.25rem spacing unit, @theme, the
dark: variant and @custom-variant, the logical-property
utilities, container queries in core, arbitrary values, and the editor tooling.
Three things are not verified and are marked as such:
-
Relative bundle sizes. Deck's 26 KB brotli is measured, from
dist/sizes.json. Tailwind's output size depends entirely on your markup, so "above a certain app size" is a judgement and no number is given for it. - Ecosystem size. Stated as a direction, not a count. Any specific number would be stale within a month.
-
Deck's browser floor. Chrome and Edge 117, Safari 17.4, Firefox 128
is what
package.jsondeclares. It has not been tested feature by feature against those exact versions, and some of what Deck uses —light-dark()in particular — may in practice need a slightly newer Safari than 17.4. Treat the declared floor as the intent and test if you are near it.
Tailwind moves quickly. If something here has gone out of date, their docs are right and this page is wrong — please say so.