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, and prints a diff instead of overwriting any file that already exists. 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 |
Icons come from @lucide/svelte, which the package already depends on.
Release channels
| Channel | Install | When to use it |
|---|---|---|
latest |
bun add @codeatlasdev/ui |
Production. Semver: breaking changes only in a major. |
beta |
bun add @codeatlasdev/ui@beta |
The next minor, feature-complete. Try it before it ships. |
alpha |
bun add @codeatlasdev/ui@alpha |
The next major while it is still moving. Expect breaking changes between alphas. |
Every version is published from CI with an npm provenance statement, so you can check which commit and workflow built it (npm audit signatures).
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. bunx @codeatlasdev/ui init reads the installed Kit version and writes the right imports.
What init writes
bunx @codeatlasdev/ui init creates the files below when they are missing. 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 |
left alone |
vite.config.ts |
caThemePlugin(theme) and caErrorPlugin({ scaffold: true }), after sveltekit(), in one @codeatlasdev/ui/vite import |
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 |
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 first and reports the other two (it never rewrites svelte.config.js or tsconfig.json).
-
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.
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: "dark" };
Import the stylesheet once in your base layout and add the viewport meta:
---
// src/layouts/Base.astro
import "@codeatlasdev/ui/styles/global.css";
---
<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" />
</head>
<body><slot /></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. In islands, call colorScheme.init() from one island that is always on the page, such as your header, or from a small script.
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.