Styling and Tailwind

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 npm install, no tailwind.config and no build step.

Tailwind, with nothing to set up

<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, or in CSS of your own. See 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:

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:

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:

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.