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/uinpm install @codeatlasdev/uipnpm add @codeatlasdev/uiThe 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
-
Register both Vite plugins.
caThemePluginbundles the ESM-only dependencies for SSR (@tanstack/svelte-table,shiki) and warns at build time if the theme script is not injected.caErrorPlugincreates the error pages when they are missing (scaffold: true), or only warns without it. Import the theme with its.tsextension: Vite loads the config with Node’s native loader, which does not resolve extensionless imports (Vite 8 warns about it). That needs"allowImportingTsExtensions": trueintsconfig.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 })], }); -
Inject the theme script and sanitize server errors in the server hook.
caThemeHandleputs 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.createErrorHandlerturns every unexpected server error into{ message, code, id }without leaking the message of a 5xx; pass alocaleor areportcallback 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(); -
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", }; -
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()} -
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" />
Plugin order does not matter
Neither plugin depends on where it sits in plugins. caThemePlugin is enforce: "pre", so Vite runs it first wherever you put it, and it looks for SvelteKit in the resolved plugin list, not in the array you wrote. caErrorPlugin only reacts to configResolved, buildStart and the dev server start. Checked with SvelteKit 3.0.1 and Vite 8.3: building with caThemePlugin and caErrorPlugin before, after and around sveltekit() gives the same result (theme script injected, error pages scaffolded, ssr.noExternal set), and in plain Vite caThemePlugin() before or after svelte() injects the script once, in build and in dev. The examples differ (after sveltekit(), before svelte()) only by convention; init follows the SvelteKit one.
Prefer app.html?
If you cannot use a hook, paste the script into src/app.html instead. CA_THEME_SCRIPT_TAG from @codeatlasdev/ui/vite is a ready-to-paste tag for the default theme; for a custom theme, use the hook.
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.
-
Configuration moves into the Vite plugin.
svelte.config.jsis ignored (config_file_unsupported) and thekitnamespace is gone (config_kit_namespace). Pass the options directly tosveltekit(), 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 })], }); -
tsconfig.jsonextends$app/tsconfig..svelte-kit/tsconfig.jsonis no longer generated (tsconfig_extends_missing):{ "extends": "$app/tsconfig" } -
$libis removed (module_removed_lib). Use the#lib/*import thatsv createadds topackage.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
#libmap, use a relative path (./lib/theme.tsfromsrc/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.
Note
Astro needs astro 5, 6 or 7 as a peer. Components that read colorScheme render correctly on the server because the init script sets class="dark" and data-preset before the stylesheet applies.
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.