Bosia Bosia v0.9.6

Styling

Built-in Tailwind CSS v4 with shadcn-inspired design tokens and dark mode.

Tailwind CSS v4

Tailwind CSS v4 is built into Bosia — no installation or configuration needed. It's compiled at build time by @tailwindcss/cli.

Your global styles live in src/app.css:

@import "tailwindcss";

@theme {
	--color-background: oklch(1 0 0);
	--color-foreground: oklch(0.145 0 0);
	--color-primary: oklch(0.205 0.064 286.3);
	--color-primary-foreground: oklch(0.985 0 0);
	/* ... more tokens */
}

Design Tokens

The default template includes shadcn-inspired semantic tokens for a consistent design system:

Token Usage
background Page background
foreground Default text color
primary Buttons, links, accents
secondary Secondary actions
muted Subtle backgrounds, disabled
accent Hover states, highlights
destructive Delete buttons, errors
card Card backgrounds
border Border colors
input Input borders
ring Focus rings

Use them with Tailwind classes:

<div class="bg-background text-foreground">
	<button class="bg-primary text-primary-foreground rounded-md px-4 py-2"> Click me </button>
	<p class="text-muted-foreground">Subtle text</p>
</div>

Dark Mode

Dark mode is activated by adding the .dark class to <html>. All design tokens have dark-mode variants defined in app.css:

.dark {
	--color-background: oklch(0.145 0 0);
	--color-foreground: oklch(0.985 0 0);
	/* ... dark variants */
}

Component styles

A <style> block inside a .svelte file is scoped by Svelte as usual. At build time Bosia harvests every one of them into a single content-hashed dist/client/bosia-css-<hash>.css, linked in the document head right after the Tailwind stylesheet.

That link is render-blocking, which is the point: a server-rendered page arrives with its own layout rules already applied, instead of painting unstyled and snapping into place once the JS bundle hydrates. Nothing is required of you — write <style> blocks normally.

Ordering is fixed: Tailwind first, component styles second. Scoped rules carry a .svelte-<hash> class so they outrank utilities on specificity anyway, but against unlayered CSS your app.css pulls in via @import, source order decides — and component styles get the last word, matching where they used to land when they were appended to <head> at hydration.

Three modes: Light, Dark, System

The theme is stored in localStorage under the theme key with one of three values:

Value Behavior
"light" Always light
"dark" Always dark
"system" Follows the OS prefers-color-scheme setting

A missing key is treated as system, so apps follow the OS preference by default. Bosia injects a small inline bootstrap script before paint that reads this value and sets .dark accordingly, so there is no flash of the wrong theme (FOUC).

The effective theme is dark when:

mode === "dark" ||
	((mode === "system" || mode == null) && matchMedia("(prefers-color-scheme: dark)").matches);

Cycle toggle

A toggle button cycles through the three modes (Light → Dark → System) and persists the choice. While in system mode it also reacts to live OS theme changes:

<script lang="ts">
	type ThemeMode = "light" | "dark" | "system";
	const ORDER: ThemeMode[] = ["light", "dark", "system"];
	let mode = $state<ThemeMode>("system");

	function applyTheme(m: ThemeMode) {
		const dark =
			m === "dark" || (m === "system" && matchMedia("(prefers-color-scheme: dark)").matches);
		document.documentElement.classList.toggle("dark", dark);
	}

	// restore the saved choice once
	$effect(() => {
		const stored = localStorage.getItem("theme") as ThemeMode | null;
		if (stored) mode = stored;
	});

	// follow live OS changes while in system mode
	$effect(() => {
		if (mode !== "system") return;
		const mq = matchMedia("(prefers-color-scheme: dark)");
		const handler = () => applyTheme("system");
		mq.addEventListener("change", handler);
		return () => mq.removeEventListener("change", handler);
	});

	function cycleTheme() {
		mode = ORDER[(ORDER.indexOf(mode) + 1) % ORDER.length];
		localStorage.setItem("theme", mode);
		applyTheme(mode);
	}
</script>

<button onclick={cycleTheme}>Theme: {mode}</button>

The bundled navbar component ships this toggle ready to use.

cn() Utility

The cn() function uses built-in class merging and tailwind-merge to safely merge Tailwind classes:

import { cn } from "$lib/utils";
// or: import { cn } from "bosia";

cn("px-4 py-2", "px-6");
// → "py-2 px-6" (px-4 removed, px-6 wins)

cn("text-red-500", isActive && "text-blue-500", className);
// → conditionally applies classes, merges conflicts

This is especially useful when building reusable components that accept a class prop:

<script lang="ts">
	import { cn } from "$lib/utils";
	let { class: className, ...props } = $props();
</script>

<button class={cn("bg-primary text-white rounded px-4 py-2", className)} {...props}>
	<slot />
</button>