Buy me a coffee

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.

  1. 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.

  2. 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.

  3. 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.

  4. 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

A button styled with 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

distill-ui's button, shortened
<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

Styling another library's button
<!-- 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

Styling a distill-ui button
<!-- 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;
}
Create an account

Every part of this card is a distill-ui component.

New

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

Read the installation guide or copy the files by hand.