Getting Started
Get up and running with dryui in minutes.
Installation
Install the full UI package (recommended) or the headless primitives only.
bun add @dryui/uiPrimitives only (no styles, no dependencies):
bun add @dryui/primitivesAI setup
Use the bundled DryUI skill for conventions and add the MCP server when you want review and diagnose tools in the same session.
From a clone of the dryui repo, link the bundled skill into Codex's skills directory (`$CODEX_HOME/skills`).
mkdir -p "$CODEX_HOME/skills"
ln -sfn "$(pwd)/skills/dryui" "$CODEX_HOME/skills/dryui"Codex setup
Restart Codex after linking the skill into $CODEX_HOME/skills. Add @dryui/mcp on the MCP Server page when you want automated review and theme diagnosis.
Setup
Import the theme CSS in your root layout or app entry point, and set class="theme-auto" on the <html> element so the system theme bootstrap works on first load.
<!-- In your root layout (+layout.svelte) or app entry -->
<script>
import '@dryui/ui/themes/default.css';
import '@dryui/ui/themes/dark.css'; // Add for dark mode + system theme support
</script><html lang="en" class="theme-auto">
<body>
%sveltekit.body%
</body>
</html>SvelteKit
Place the imports in your root +layout.svelte or app.css so the theme applies globally. Import default.css always. Add dark.css when you want automatic light/dark switching or a manual theme toggle.
Keep theme-auto on <html> until the user explicitly chooses light or dark.
Local linking
If you are working against a linked checkout with file: or npm link, DryUI now ships explicit subpath exports for the common linked-development path. If your toolchain still resolves the symlink boundary incorrectly, use the alias recipe below. The safest path inside the monorepo is still workspace:*.
// vite.config.ts
import { defineConfig } from 'vite';
import { fileURLToPath, URL } from 'node:url';
const uiSourceRoot = fileURLToPath(new URL('../../packages/ui/src/', import.meta.url));
const primitiveSourceRoot = fileURLToPath(new URL('../../packages/primitives/src/', import.meta.url));
export default defineConfig({
resolve: {
alias: [
{ find: /^@dryui\/ui$/, replacement: fileURLToPath(new URL('../../packages/ui/src/index.ts', import.meta.url)) },
{ find: /^@dryui\/ui\/(.*)$/, replacement: uiSourceRoot + '$1' },
{ find: /^@dryui\/primitives$/, replacement: fileURLToPath(new URL('../../packages/primitives/src/index.ts', import.meta.url)) },
{ find: /^@dryui\/primitives\/(.*)$/, replacement: primitiveSourceRoot + '$1' },
],
},
});Linked checkout workaround
The alias recipe above routes both the package root and subpaths back to source files, which keeps subpath imports working during local development when a bundler still struggles with linked packages. If you do not want to alias, fall back to the root barrel import from @dryui/ui while you iterate.
CLI shortcuts
The CLI now includes init for setup snippets, add for component starters, and get for copyable composed output source.
node packages/cli/dist/index.js init
node packages/cli/dist/index.js add Card --with-theme
node packages/cli/dist/index.js get "Checkout Forms"Basic Usage
Button
<script>
import { Button } from '@dryui/ui';
</script>
<Button variant="solid" onclick={() => alert('Hello!')}>
Click me
</Button>Card
<script>
import { Card } from '@dryui/ui';
</script>
<Card.Root>
<Card.Header>
<h3>My Card</h3>
</Card.Header>
<Card.Content>
<p>Card content goes here.</p>
</Card.Content>
</Card.Root>Form Validation
Use SvelteKit form actions for server-side validation and surface the result through Field.Root error plus Field.Error. Keep the native name on the input so the browser and form actions still submit the field correctly.
Server action
import { fail } from '@sveltejs/kit';
export const actions = {
default: async ({ request }) => {
const data = await request.formData();
const email = String(data.get('email') ?? '').trim();
const errors: Record<string, string> = {};
if (!email.includes('@')) {
errors.email = 'Enter a valid email address.';
}
if (Object.keys(errors).length > 0) {
return fail(400, { errors, values: { email } });
}
return { success: true };
},
};Form
<script lang="ts">
import { enhance } from '$app/forms';
import { Button, Field, Input, Label } from '@dryui/ui';
import type { ActionData } from './$types';
let { form }: { form: ActionData | undefined } = $props();
</script>
<form method="POST" use:enhance>
<Field.Root error={form?.errors?.email}>
<Label for="email">Email</Label>
<Input
id="email"
name="email"
type="email"
value={form?.values?.email ?? ''}
autocomplete="email"
/>
<Field.Error>{form?.errors?.email}</Field.Error>
</Field.Root>
<Button type="submit">Continue</Button>
</form>Validation pattern
Return field-specific errors from fail(), then bind them into the error prop on Field.Root. Render Field.Error when you want the message visible in the layout as well as in the field wrapper state.
Dark Mode
Default to the browser or OS preference with class="theme-auto" on your <html> element. Use data-theme only for explicit light/dark
overrides; system mode is theme-auto with no stored dryui-docs-theme value.
Recommended default:
<html class="theme-auto"><!-- Force dark mode -->
<html data-theme="dark">
<!-- Force light mode -->
<html data-theme="light">Tip
If you build a theme toggle, keep system mode as the default. Store explicit light or dark choices under dryui-docs-theme, and
clear the key plus data-theme when the user returns to system mode.
const STORAGE_KEY = 'dryui-docs-theme';
const root = document.documentElement;
function applyTheme(preference) {
if (preference === 'system') {
delete root.dataset.theme;
root.classList.add('theme-auto');
localStorage.removeItem(STORAGE_KEY);
return;
}
root.dataset.theme = preference;
root.classList.remove('theme-auto');
localStorage.setItem(STORAGE_KEY, preference);
}
const storedTheme = localStorage.getItem(STORAGE_KEY);
applyTheme(storedTheme === 'light' || storedTheme === 'dark' ? storedTheme : 'system');Architecture
dryui is organised into three independent layers you can adopt at any level.
Headless, unstyled components. Zero dependencies. Full control over styling.
Styled components built on primitives. Includes a CSS-variable theme system.
AI tooling: the dryui command for component lookup + MCP tools for review and diagnose.
Customization
Every visual property is a CSS variable. Override globally or scope to specific elements.
Global overrides
:root {
--dry-color-primary: #8b5cf6;
--dry-color-primary-hover: #7c3aed;
--dry-radius-md: 12px;
}Per-component overrides
.my-button {
--dry-btn-bg: #8b5cf6;
--dry-btn-radius: 9999px;
}Next steps
Browse the component docs in the sidebar to see every available component, its props, and live examples.