Skip to content
CodeAtlas UI
On this page Release channels

Getting started

Installation

Install @codeatlasdev/ui and wire the theme into SvelteKit, Astro or plain Vite without a flash of the wrong theme.

CodeAtlas UI needs Svelte 5 (runes mode). In a SvelteKit or Astro app, bunx @codeatlasdev/ui init does the whole setup below for you (see The init command). Setup is three things: import the stylesheet once, inject the theme script before first paint, and start the colorScheme store in your root layout.

bun add @codeatlasdev/ui

The core bundle stays small. Heavy features are separate entry points with optional peer dependencies, so install only what you use:

Feature Import from Also install
Charts @codeatlasdev/ui/charts echarts
DataTable @codeatlasdev/ui/table @tanstack/svelte-table
CodeSnippet @codeatlasdev/ui/code shiki
Masked and money fields @codeatlasdev/ui/forms nothing
Accessibility test helpers @codeatlasdev/ui/testing axe-core
Brand icons (caBrandPlugin) @codeatlasdev/ui/vite @resvg/resvg-js and png-to-ico
migrate command bunx @codeatlasdev/ui migrate typescript

@sveltejs/kit and astro are optional peers too: install the one your app uses. Icons come from @lucide/svelte, which the package already depends on.

Release channels

Releases are published by version tag (@codeatlasdev/ui@X.Y.Z), not from branches. The prerelease id in the version is the npm dist-tag.

Version Install When to use it
X.Y.Z bun add @codeatlasdev/ui (dist-tag latest) Production. Semver: breaking changes only in a major.
X.Y.Z-beta.N bun add @codeatlasdev/ui@beta A prerelease of the next version. It never moves latest. Try it before it ships.

A prerelease with another id (X.Y.Z-alpha.N, for example) is published under a dist-tag with that id. Pin an exact version (bun add @codeatlasdev/ui@3.1.0-beta.2) to keep a prerelease stable.

Every version is published from CI with an npm provenance statement, so you can check which commit and workflow built it (npm audit signatures).

The init command

bunx @codeatlasdev/ui init [--write] [--dir <path>]

init detects the framework from package.json (@sveltejs/kit means SvelteKit, otherwise astro means Astro) and prints Detected SvelteKit. or Detected Astro.. With neither it prints an error and exits with code 1. Then it goes through the files of that framework, one line each:

Line Meaning
create <file> The file was missing and is now written.
ok <file> (already set up) Nothing to change. Running init again on a finished setup prints only ok lines.
diff <file> (exists, not modified) The file exists and needs a change. A unified diff follows; nothing is written until you re-run with --write.
patch <file> The change was applied (--write).
manual <file>: could not patch safely. init could not edit the file safely (no plugins: [...] array, an existing handle of your own, no </body>). It says what to add by hand and exits with code 1.
skip <file> (not found) The file does not exist and init does not create it (vite.config.ts, src/app.html and the Astro config).
Flag Effect
--write Apply the proposed changes to existing files. Files that do not exist yet are always created.
--dir <path> Project directory. Defaults to the current directory.

When diffs are pending it ends with N existing file(s) left untouched. Re-run with --write to apply the diffs above. A clean run ends with Done. Run your dev server: the theme paints on the first frame.

SvelteKit

@codeatlasdev/ui supports SvelteKit 2 (^2.12) and SvelteKit 3. The steps below are the same on both; the differences are in SvelteKit 3. init reads the installed Kit major (node_modules/@sveltejs/kit, or the range in package.json before install ran; anything it cannot parse counts as 2) and writes the right theme import.

What init writes

For each file below, init creates it when it is missing, except vite.config.ts and src/app.html, which it only patches (it prints skip when they are not there). A file that already exists is never overwritten: init prints a diff and applies it only with --write. Re-running it changes nothing once the setup is complete.

File What init puts there If the file exists
src/lib/theme.ts the shared theme object (defaultPreset: "amber", defaultScheme: "system") left alone
vite.config.ts caThemePlugin(theme) and caErrorPlugin({ scaffold: true }), appended to plugins, in one @codeatlasdev/ui/vite import (plus import { theme } from "./src/lib/theme.ts") adds only the plugin that is missing
src/hooks.server.ts handle = sequence(caThemeHandle(theme)) and handleError = createErrorHandler() adds only what is missing; reports manual if you already define a handle of your own; never touches your handleError
src/routes/+layout.svelte the stylesheet import and colorScheme.init() in onMount adds only the missing import or call
src/app.html the viewport meta with viewport-fit=cover, interactive-widget=resizes-content rewrites only the viewport meta; reports manual when there is none

caErrorPlugin({ scaffold: true }) then creates src/routes/+error.svelte and src/error.html the first time Vite runs (dev or build), and never overwrites them. The result is the setup below, which you can also write by hand.

Full setup

  1. Register both Vite plugins. caThemePlugin bundles the ESM-only dependencies for SSR (@tanstack/svelte-table, shiki) and warns at build time if the theme script is not injected. caErrorPlugin creates the error pages when they are missing (scaffold: true), or only warns without it. Import the theme with its .ts extension: Vite loads the config with Node’s native loader, which does not resolve extensionless imports (Vite 8 warns about it). That needs "allowImportingTsExtensions": true in tsconfig.json; SvelteKit’s generated base config already sets it.

    // vite.config.ts
    import { sveltekit } from "@sveltejs/kit/vite";
    import { caErrorPlugin, caThemePlugin } from "@codeatlasdev/ui/vite";
    import { defineConfig } from "vite";
    import { theme } from "./src/lib/theme.ts";
    
    export default defineConfig({
      plugins: [sveltekit(), caThemePlugin(theme), caErrorPlugin({ scaffold: true })],
    });
  2. Inject the theme script and sanitize server errors in the server hook. caThemeHandle puts the brand presets and the init script right after <head> of every SSR and prerendered page, so the first paint already has the right scheme and preset. createErrorHandler turns every unexpected server error into { message, code, id } without leaking the message of a 5xx; pass a locale or a report callback when you need them.

    // src/hooks.server.ts
    import { sequence } from "@sveltejs/kit/hooks";
    import { caThemeHandle, createErrorHandler } from "@codeatlasdev/ui/kit";
    import { theme } from "$lib/theme";
    
    export const handle = sequence(caThemeHandle(theme));
    export const handleError = createErrorHandler();
  3. Define the theme once. Both the hook and the plugin take the same object. An empty object is valid and gives you the built-in amber preset in dark mode.

    // src/lib/theme.ts
    import type { ThemeOptions } from "@codeatlasdev/ui/vite";
    
    export const theme: ThemeOptions = {
      defaultScheme: "system",
    };
  4. Import the stylesheet and start the store in the root layout.

    <!-- src/routes/+layout.svelte -->
    <script>
      import "@codeatlasdev/ui/styles/global.css";
      import { colorScheme } from "@codeatlasdev/ui";
      import { onMount } from "svelte";
    
      let { children } = $props();
      onMount(() => colorScheme.init());
    </script>
    
    {@render children()}
  5. Set the viewport meta in src/app.html. See Mobile for what each part does.

    <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover, interactive-widget=resizes-content" />

SvelteKit 3

Kit 3 changed three things this setup touches. init handles the third (the theme import in the hook) and, for the other two, prints a note line instead of rewriting svelte.config.js or tsconfig.json: it reports svelte.config.js/.ts/.mjs when one exists, and a tsconfig.json that does not mention $app/tsconfig. On a clean Kit 3 run you get no Done. line while a note is pending.

  1. Configuration moves into the Vite plugin. svelte.config.js is ignored (config_file_unsupported) and the kit namespace is gone (config_kit_namespace). Pass the options directly to sveltekit(), next to the two plugins:

    // vite.config.ts
    import adapter from "@sveltejs/adapter-auto";
    import { sveltekit } from "@sveltejs/kit/vite";
    import { caErrorPlugin, caThemePlugin } from "@codeatlasdev/ui/vite";
    import { defineConfig } from "vite";
    import { theme } from "./src/lib/theme.ts";
    
    export default defineConfig({
      plugins: [sveltekit({ adapter: adapter() }), caThemePlugin(theme), caErrorPlugin({ scaffold: true })],
    });
  2. tsconfig.json extends $app/tsconfig. .svelte-kit/tsconfig.json is no longer generated (tsconfig_extends_missing):

    { "extends": "$app/tsconfig" }
  3. $lib is removed (module_removed_lib). Use the #lib/* import that sv create adds to package.json ("imports": { "#lib/*": "./src/lib/*" }), written with the extension. In the hook, import the theme as #lib/theme.ts:

    // src/hooks.server.ts
    import { theme } from "#lib/theme.ts";

    Without a #lib map, use a relative path (./lib/theme.ts from src/hooks.server.ts). Everything else (the layout, the stylesheet import, colorScheme.init() and the viewport meta) is identical to Kit 2.

Astro

Use the integration together with @astrojs/svelte. caUI injects the theme into the <head> of every page and handles the SSR bundling. bunx @codeatlasdev/ui init sets this up for an Astro project:

File What init puts there If the file exists
src/theme.ts the shared theme object (defaultPreset: "amber", defaultScheme: "system") left alone
astro.config.mjs (or .ts, .js, .mts) caUI(theme) first in integrations, with its two imports adds caUI(theme) unless caUI( is already there; reports manual when there is no integrations: [ array
the first .astro file in src/layouts that renders <html> the stylesheet import, the viewport meta and a client <script> that calls colorScheme.init() adds only what is missing; reports manual when there is no </body> for the script
src/layouts/Base.astro created only when src/layouts has no layout that renders <html> not applicable
bun add @codeatlasdev/ui @astrojs/svelte svelte
// astro.config.mjs
import svelte from "@astrojs/svelte";
import { caUI } from "@codeatlasdev/ui/astro";
import { defineConfig } from "astro/config";
import { theme } from "./src/theme.ts";

export default defineConfig({ integrations: [svelte(), caUI(theme)] });
// src/theme.ts
import type { ThemeOptions } from "@codeatlasdev/ui/astro";

export const theme: ThemeOptions = { defaultPreset: "amber", defaultScheme: "system" };

Import the stylesheet once in your base layout, add the viewport meta and start the store from a client script, so theme switches and OS scheme changes stay in sync on the page:

---
// src/layouts/Base.astro
import "@codeatlasdev/ui/styles/global.css";
---
<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover, interactive-widget=resizes-content" />
    <title>My app</title>
  </head>
  <body>
    <slot />
    <script>
      import { colorScheme } from "@codeatlasdev/ui";
      colorScheme.init();
    </script>
  </body>
</html>

Components are Svelte islands. Add a client directive to anything interactive (client:visible or client:load); purely visual components render on the server with no JavaScript. Call colorScheme.init() once per page, from that layout script or from one island that is always present.

Plain Vite

caThemePlugin also works without SvelteKit: it injects the theme head through transformIndexHtml.

// vite.config.ts
import { svelte } from "@sveltejs/vite-plugin-svelte";
import { caThemePlugin } from "@codeatlasdev/ui/vite";
import { defineConfig } from "vite";

export default defineConfig({ plugins: [caThemePlugin(), svelte()] });
// src/main.ts
import "@codeatlasdev/ui/styles/global.css";
import { colorScheme } from "@codeatlasdev/ui";
import { mount } from "svelte";
import App from "./App.svelte";

colorScheme.init();
mount(App, { target: document.getElementById("app")! });

Verify

<script>
  import { Button, ThemeToggle } from "@codeatlasdev/ui";
</script>

<Button variant="filled">It works</Button>
<ThemeToggle />

Reload with the toggle on dark and check that there is no white flash. Continue with Theming.