---
title: Styling and Tailwind
description: Tailwind out of the box, with nothing to install or configure; theme tokens, your own CSS, and cn to join and merge classes.
section: Frontend
order: 2
---

# Styling and Tailwind

<p class="lead">Tailwind works out of the box. Write its classes anywhere in your app and they're styled: Sluurp reads the classes your files use and serves the CSS for them. There's no <code>npm install</code>, no <code>tailwind.config</code> and no build step.</p>

## Tailwind, with nothing to set up

```tsx
<button class="rounded-lg bg-primary px-4 py-2 text-primary-foreground hover:opacity-90 md:px-6">Save</button>
```

Every page loads two stylesheets:

- `/_/theme.css`: the theme, meaning colours, fonts and radii as CSS variables, for light and dark.
- `/_/tw.css`: the utilities your app uses. It's made from your files as they are now, and made again as you save, while the page updates in place.

What Tailwind offers is there:

- variants such as `hover:`, `focus-visible:`, `md:`, `lg:` and `dark:`;
- arbitrary values, such as `w-[312px]` and `bg-[#0d99ff]`;
- opacity, as in `bg-primary/80`;
- and the rest of Tailwind's classes.

A class chosen while the app runs, one that isn't written in any file, can be styled too, with `useClasses` from `sluurp/classes`.

## Theme tokens

Colours are named for what they're for, as shadcn/ui names them, so light and dark themes, and your own palette, change them all at once:

`bg-background` `text-foreground` `bg-card` `bg-muted` `text-muted-foreground` `bg-primary` `text-primary-foreground` `bg-accent` `border-border` `ring-ring` `bg-destructive`

Change them for your app with the [Palette Editor](/docs/ui-kit#palette-editor), or in CSS of your own. See [Theming](/docs/ui-kit#theming).

## Your own CSS

A `.css` file in your app is served as it is. Link it from your layout or your `index.html`. It swaps in place as you save, without a reload.

Scroll-in motion needs no script: `motion-fade-in`, `motion-fade-up`, `motion-scale-in`, `motion-slide-left`, `motion-slide-right` and `motion-blur-in` make an element come in as it scrolls into view.

## cn: joining and merging classes

`cn` joins classes and settles conflicts between them:

```tsx
import { cn } from "sluurp/cn";

cn("px-2 py-1", active && "bg-primary", { "text-white": active }, className);
```

- **Joining**: it takes strings, arrays, and objects whose true keys are kept. `false`, `null` and `undefined` disappear, as with `clsx`.
- **Merging**: when two classes set the same thing, the last one wins, as with `tailwind-merge`. `cn("px-2", "px-4")` is `"px-4"`, while `cn("px-2", "py-4")` keeps both.
- **Variants**: `hover:px-2` and `px-4` don't conflict, because they apply at different times.

That's what a component needs so its caller can change its look:

```tsx
function Card({ className, children }) {
  return <div class={cn("rounded-xl border p-4", className)}>{children}</div>;
}

<Card className="p-8">…</Card>   // p-8 wins over p-4
```

`cn` knows which classes conflict from the same pass that makes your stylesheet, so it's never out of step with it. A class it doesn't know, such as one from your own CSS, is always kept, never dropped.

## variants: a component's looks, by name

`variants` gives a component named choices, in the same shape as `class-variance-authority`, so shadcn/ui's components port as they are:

```tsx
import { variants } from "sluurp/cn";

const button = variants("inline-flex items-center rounded-md font-medium", {
  variants: {
    variant: { default: "bg-primary text-primary-foreground", ghost: "hover:bg-accent" },
    size: { sm: "h-8 px-3 text-sm", lg: "h-10 px-6" },
  },
  defaultVariants: { variant: "default", size: "sm" },
});

<button class={button({ variant: "ghost", size: "lg", className: "w-full" })}>Open</button>
```

The caller's `className` comes last, so it wins.
