---
title: "Style an app with Tailwind"
audience: "anyone who styles with Tailwind"
description: "You'll style a status page with Tailwind utilities. Tailwind CSS v4 is built into the bundler and the build hands it the exact list of classes your pages…"
snippet_caption: ""
snippet: ""
---

import { Install } from "@src/ui/Install";

You'll style a status page with Tailwind utilities. Tailwind CSS v4 is built into the bundler and the build hands it the exact list of classes your pages render. It also tells you about a class it can't see before a visitor finds the missing style.

<Install captured="0.x" />

## Start an app with Tailwind in it

```shell-session
$ fsr new status --with tailwind
$ ls status/app
generated
importmap.json
routes
src
styles
tsconfig.build.json
tsconfig.json
types
```

The compiler is `snapfire_tailwind`, Tailwind CSS v4 ported to Rust, inside the bundler you already installed. `app/styles/app.css` opts in on its first line:

```css
@import "tailwindcss";
```

The rest of that file is the scaffold's own CSS: a `--line` color with a dark value and a few classes the layout uses. Tailwind and plain CSS sit in one stylesheet.

## A page with a mistake in it

Some services to report on, in `app/src/services.ts`:

```ts title="app/src/services.ts"
export type State = "operational" | "degraded" | "down";
export type Service = { name: string; state: State; uptime: string };

export const SERVICES: Service[] = [
  { name: "API", state: "operational", uptime: "99.98%" },
  { name: "Checkout", state: "degraded", uptime: "99.41%" },
  { name: "Search", state: "operational", uptime: "99.95%" },
  { name: "Email", state: "down", uptime: "97.12%" },
];
```

```ts title="app/routes/page.loader.ts"
import type { Ctx } from "@snapfire/fsr";
import { SERVICES } from "@src/services";

export async function load(_ctx: Ctx<"/">) {
  return { services: SERVICES };
}
```

Now the page, with two mistakes in it:

```tsx title="app/routes/page.tsx"
import type { RootProps } from "@generated/client";

export default function Index({ services }: RootProps) {
  return (
    <section>
      <h1 className="mb-6 text-3xl font-bold">System status</h1>
      <ul className="grid gap-3">
        {services.map((s) => (
          <li key={s.name} className="flex items-center justify-between rounded-xl border border-line p-4">
            <span className="font-medium">{s.name}</span>
            <span className={`rounded-full px-3 py-1 text-sm status-${s.state}`}>{s.state}</span>
          </li>
        ))}
      </ul>
    </section>
  );
}
```

An app made with `--with tailwind` alone writes its pages in fsr's own template dialect, which the server renders and the browser never runs.

```shell-session
$ fsr dev status/app --verbose
tailwind  22 classes in generated/tailwind.classes
warning   routes/page.tsx#default            tailwind/unlisted-class: a class value the build cannot list, `rounded-full px-3 py-1 text-sm status-${s.state}`
⚠️  `border-line` in routes/page.tsx#default is not a Tailwind utility and no stylesheet defines it
```

Those are the lines about Tailwind from the build's output.

## Where the 22 classes came from

fsr already lowered every page to find out what it renders, so it reads the classes off that: every `className`, both branches of a conditional and the fixed parts of a template string. They're written to `generated/tailwind.classes`, one per line with the module that uses it:

```shell-session
$ head -5 status/app/generated/tailwind.classes
bar	routes/layout.tsx#default
bg-amber-100	routes/page.tsx#default
bg-emerald-100	routes/page.tsx#default
bg-rose-100	routes/page.tsx#default
border	routes/page.tsx#default
```

The stylesheet is built for exactly that list. That's also why the two warnings can exist at all: the build knows what it couldn't list.

## Fix the two warnings

`status-${s.state}` is a class made from data. The build can't know which states exist, so neither can the stylesheet. Those pills would have no color. Write the values where the build can read them. A lookup object is read whole, so all three classes make the list. Replace `app/routes/page.tsx`:

```tsx title="app/routes/page.tsx" {3-7,17}
import type { RootProps } from "@generated/client";

const tone: Record<string, string> = {
  operational: "bg-emerald-100 text-emerald-800",
  degraded: "bg-amber-100 text-amber-800",
  down: "bg-rose-100 text-rose-800",
};

export default function Index({ services }: RootProps) {
  return (
    <section>
      <h1 className="mb-6 text-3xl font-bold">System status</h1>
      <ul className="grid gap-3">
        {services.map((s) => (
          <li key={s.name} className="flex items-center justify-between rounded-xl border border-line p-4">
            <span className="font-medium">{s.name}</span>
            <span className={`rounded-full px-3 py-1 text-sm ${tone[s.state]}`}>{s.state}</span>
          </li>
        ))}
      </ul>
    </section>
  );
}
```

`border-line` is a typo for a color Tailwind doesn't have. The scaffold's `--line` variable is plain CSS, so Tailwind has never heard of it. Make it a theme color by adding this to `app/styles/app.css`, right below `@import "tailwindcss";`:

```css title="app/styles/app.css"
@theme inline {
  --color-line: var(--line);
}
```

`inline` makes `border-line` use `var(--line)` where it's applied, so the dark value the scaffold already sets still switches with the system theme. Save both files and `fsr dev` rebuilds the stylesheet:

```shell-session
tailwind  28 classes in generated/tailwind.classes
```

No warnings. Open `http://127.0.0.1:3000`: four rounded rows, each with a green, amber or rose pill on the right.

## Add a class while it runs

Add `hover:shadow-md` to the end of the `li`'s `className` and save:

```shell-session
tailwind  29 classes in generated/tailwind.classes
```

The page reloads and the rows lift under the pointer. The stylesheet only ever holds what the pages use. `app/dist/styles/app.css` is about 10 KB here, with only the theme values those 29 classes need.

## Try it

Change `flex` on the `li` to `flx` and save. The build names it as you go:

```shell-session
⚠️  `flx` in routes/page.tsx#default is not a Tailwind utility and no stylesheet defines it
```

Then put `status-${s.state}` back in place of `${tone[s.state]}`. The `tailwind/unlisted-class` warning comes back and the pills lose their color, since no stylesheet has a rule for a class nobody could list. Put the lookup back.
