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.

The spacing scale and the breakpoints are the same numbers

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.

Common Tailwind utilities and their Deck equivalents
TailwindDeckNotes
p-4, px-4, py-4.p-4, .px-4, .py-4Same value. Deck has only 0,1,2,3,4,6,8
ml-4, mr-4.mis-4, .mie-4Logical only. No physical version exists
space-y-4.stack-4A real gap, not a sibling selector
flex items-center gap-3.clusterAlso wraps by default
grid grid-cols-3 gap-4.gridAuto-fits from a min width; no column count
text-sm, text-lg.text-sm, .text-lgSame names, different values
font-semibold.fw-semi620, not 600 — the face is variable
text-gray-500.text-mutedA role, so it follows dark mode
bg-white.bg-surfaceAs above
rounded-md, shadow-sm.r-md, .shadow-1Five radius steps, five shadow steps
md:flex, lg:hiddenmd:flex, lg:hiddenSame widths. Deck's set is fixed, not generated
sr-only.sr-onlyIdentical
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.

  1. 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.
  2. Class names you cannot derive. Tailwind's are mechanical: knowing p, t and the scale gets you pt-7 without 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.
  3. 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.
  4. 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.pages or a style attribute, which is more keystrokes and puts the value somewhere other than where you are working.
  5. 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.css is 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.
  6. 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(), @container style queries and :has(). If you have to support Safari 16, Deck is not an option.
  7. 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

  1. 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.
  2. 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.
  3. Retheming from one number. Covered above. If you sell to businesses who want their own colours, this is a feature you would otherwise build.
  4. 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.
  5. 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.

  1. 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.
  2. Convert one page, not one component. Deck's value is in the primitives composing, so a .card dropped into a Tailwind flex column shows you the smallest part of the benefit and all of the friction.
  3. 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.
  4. Delete the dark: variants as you go. Once an element uses Deck's role tokens it has a dark mode, and the leftover dark: classes will fight it.
  5. 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.json declares. 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.