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:anddark:; - arbitrary values, such as
w-[312px]andbg-[#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,nullandundefineddisappear, as withclsx. - Merging: when two classes set the same thing, the last one wins, as with
tailwind-merge.cn("px-2", "px-4")is"px-4", whilecn("px-2", "py-4")keeps both. - Variants:
hover:px-2andpx-4don’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-4cn 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.