Svelte 5 · Plain CSS · No Tailwind
Scoped CSS, the way Svelte intended.
distill-ui is a set of accessible components you copy into your Svelte project and style with
a normal <style> block: pure CSS, the way it was meant to be written. No
utility classes, no :global(), no workarounds.
Why this exists
Component libraries stopped writing CSS.
Most Svelte component libraries are built on Tailwind. That works for a lot of people. But if you'd rather write CSS, you're left with headless libraries that render their parts somewhere your scoped styles can't reach. The usual fixes are band-aids, not solutions. distill-ui is for people who want control, clean separation, and scoped Svelte styles without the workarounds.
-
Svelte, used the way it was designed
Svelte scopes the CSS in each component for you. distill-ui leans on that instead of working around it: no :global(), no child snippets, no build plugin.
-
Markup in the markup, styles in the style block
Your template says what the thing is. The style block says what it looks like. Neither one is buried in the other.
-
Readable by anyone who knows CSS
background: var(--dui-color-primary) reads like what it does. There is no class vocabulary to learn and nothing to decode.
-
Yours to change
Components are copied into your project, not hidden in node_modules. Change a token, set an option from a parent, or rewrite the whole style block.
Readable
More lines, less to decode.
A style block is longer than a class string. It's also something you can read top to bottom, search, and change one line of without breaking the rest.
Utility classes
<button
class="inline-flex items-center justify-center gap-2
whitespace-nowrap rounded-md text-sm font-medium
transition-all disabled:pointer-events-none
disabled:opacity-50 bg-primary text-primary-foreground
shadow-xs hover:bg-primary/90 h-9 px-4 py-2
has-[>svg]:px-3 outline-none focus-visible:border-ring
focus-visible:ring-ring/50 focus-visible:ring-[3px]"
>
Save
</button>
distill-ui
<button class="button">Save</button>
<style>
.button {
height: 2.25rem;
padding-inline: var(--dui-space-2-5);
border-radius: var(--dui-radius-md);
background: var(--dui-color-primary);
color: var(--dui-color-primary-foreground);
&:hover {
background: var(--dui-color-primary-hover);
}
&:disabled {
opacity: 0.5;
}
}
</style>
Scoped
Your selector reaches the element.
Headless libraries render each part inside their own components, so Svelte never sees the
element your selector is aimed at. The fixes are :global(), which leaks your
styles to the whole app, or a child snippet around every part you want to style. distill-ui
writes every element in the component's own file, already styled. To change it, update the
styles directly in the component's source file.
Headless library
<!-- Workaround 1: leak the style to the
whole app with :global(). -->
<Button class="save">Save</Button>
<style>
:global(.save) {
border-radius: 9999px;
}
</style>
<!-- Workaround 2: render the button yourself
with a child snippet, so a scoped .save rule
reaches it. Repeat everywhere you use one. -->
<Button>
{#snippet child({ props })}
<button {...props} class="save">Save</button>
{/snippet}
</Button>
distill-ui
<!-- In your app -->
<Button>Save</Button>
<!-- In button.svelte, which lives in your project -->
<button class="button">
{@render children?.()}
</button>
<style>
/* Change the scoped style here. It just works. */
.button {
border-radius: 9999px;
}
</style>
Try it
Three variables. Every component.
Colors, corners and spacing are CSS custom properties. Drag a slider and the whole card follows, because every part reads the same tokens.
:root {
--dui-color-primary: oklch(0.55 0.12 225);
--dui-color-primary-foreground: white;
--dui-color-ring: oklch(0.55 0.12 225);
--dui-radius: 0.625rem;
}Every part of this card is a distill-ui component.
20 components
Buttons, forms, dialogs, menus, selects, tabs, toasts and more, with more on the way.
Accessible by default
Native elements where the browser does the job, Melt UI for dialogs, menus and selects. Checked with axe in light and dark.
Dark mode built in
Follows the system setting, or set it yourself with one attribute on the page.
Restyle from anywhere
Set --dui-button-bg on any parent, in your own scoped CSS, and it just applies.
Start in two commands
npx distill-ui init
npx distill-ui add button dialog