Theme

Getting Started

Get up and running with dryui in minutes.

Installation

Install the full UI package (recommended) or the headless primitives only.

bash
bun add @dryui/ui

Primitives only (no styles, no dependencies):

bash
bun add @dryui/primitives

AI 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`).

bash
mkdir -p "$CODEX_HOME/skills"
ln -sfn "$(pwd)/skills/dryui" "$CODEX_HOME/skills/dryui"

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.

svelte
<!-- 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
<html lang="en" class="theme-auto">
  <body>
    %sveltekit.body%
  </body>
</html>

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:*.

ts
// 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' },
    ],
  },
});
bash
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

svelte
<script>
  import { Button } from '@dryui/ui';
</script>

<Button variant="solid" onclick={() => alert('Hello!')}>
  Click me
</Button>

Card

svelte
<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

ts
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

svelte
<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>

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
<html class="theme-auto">
html
<!-- Force dark mode -->
<html data-theme="dark">

<!-- Force light mode -->
<html data-theme="light">
js
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.

@dryui/primitives

Headless, unstyled components. Zero dependencies. Full control over styling.

@dryui/ui

Styled components built on primitives. Includes a CSS-variable theme system.

@dryui/cli @dryui/mcp

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

css
:root {
  --dry-color-primary: #8b5cf6;
  --dry-color-primary-hover: #7c3aed;
  --dry-radius-md: 12px;
}

Per-component overrides

css
.my-button {
  --dry-btn-bg: #8b5cf6;
  --dry-btn-radius: 9999px;
}