Styling
Honeycomb uses Tailwind CSS v4 with the Vite plugin, shadcn-svelte design tokens in OKLCH color space, and a dark-mode system powered by mode-watcher. All styling configuration lives in a single CSS file with no tailwind.config — Tailwind v4 moves everything into CSS.
Vite plugin
Section titled “Vite plugin”Tailwind v4 replaces the PostCSS plugin with a first-class Vite plugin. It is registered in vite.config.ts alongside SvelteKit:
import tailwindcss from '@tailwindcss/vite';import { sveltekit } from '@sveltejs/kit/vite';import { defineConfig } from 'vite';
export default defineConfig({ plugins: [tailwindcss(), sveltekit()] });There is no tailwind.config.js or postcss.config.js. All theme values are declared directly in CSS.
Global stylesheet
Section titled “Global stylesheet”The single source of truth for design tokens is src/routes/layout.css. It is imported by the root +layout.svelte and referenced by the shadcn-svelte CLI via components.json.
@import "tailwindcss";@import "tw-animate-css";
@custom-variant dark (&:is(.dark *));Key points:
@import "tailwindcss"— the v4 replacement for the old@tailwinddirectives.tw-animate-css— provides animation utility classes used by shadcn-svelte components (enter/exit transitions, accordion slides, etc.).@custom-variant dark— defines dark mode as a class strategy. Elements inside a parent with the.darkclass receive dark styles.
Design tokens
Section titled “Design tokens”All colors use the OKLCH color space for perceptually uniform blending. Tokens are split into :root (light) and .dark overrides.
Light mode (:root)
Section titled “Light mode (:root)”| Token | OKLCH value | Usage |
|---|---|---|
--background | oklch(0.98 0.005 270) | Page background |
--foreground | oklch(0.18 0.04 265) | Default text |
--primary | oklch(0.55 0.22 265) | Buttons, links, active states |
--primary-foreground | oklch(1 0 0) | Text on primary |
--secondary | oklch(0.95 0.02 270) | Secondary surfaces |
--muted | oklch(0.96 0.01 270) | Disabled / subtle backgrounds |
--muted-foreground | oklch(0.50 0.03 265) | Placeholder text |
--accent | oklch(0.93 0.04 270) | Hover highlights |
--destructive | oklch(0.60 0.24 25) | Error / delete states |
--border | oklch(0.92 0.015 270) | Borders and dividers |
--ring | oklch(0.55 0.22 265) | Focus rings |
Dark mode (.dark)
Section titled “Dark mode (.dark)”The dark palette follows Material Design elevation principles — surfaces gain white-overlay lightness at higher elevations:
| Token | OKLCH value | Elevation note |
|---|---|---|
--background | oklch(0.15 0.005 250) | Base (#121212) |
--card | oklch(0.22 0.005 250) | 1dp — 5% white overlay |
--popover | oklch(0.27 0.005 250) | 8dp — 12% white overlay |
--accent | oklch(0.24 0.005 250) | 2dp — 7% white overlay |
--foreground | oklch(0.92 0.005 250) | 87% white (high emphasis) |
--muted-foreground | oklch(0.65 0.005 250) | 60% white (medium emphasis) |
Theme bridge (@theme inline)
Section titled “Theme bridge (@theme inline)”Tailwind v4 requires an explicit @theme inline block to map CSS variables into its utility system. This is what lets you write bg-primary or text-muted-foreground:
@theme inline { --radius-sm: calc(var(--radius) - 4px); --radius-md: calc(var(--radius) - 2px); --radius-lg: var(--radius); --radius-xl: calc(var(--radius) + 4px); --color-background: var(--background); --color-foreground: var(--foreground); --color-primary: var(--primary); --color-primary-foreground: var(--primary-foreground); /* ... all tokens mapped ... */}The base radius is --radius: 0.625rem (10px). Four derived sizes are generated from it.
Chart colors
Section titled “Chart colors”Five chart tokens (--chart-1 through --chart-5) provide a coordinated palette for data visualizations. They shift between modes to maintain contrast on their respective backgrounds.
Sidebar tokens
Section titled “Sidebar tokens”A dedicated set of --sidebar-* tokens allows the sidebar component to use its own surface, text, accent, and border colors independently from the main layout.
Base layer
Section titled “Base layer”@layer base { * { @apply border-border outline-ring/50; } body { @apply bg-background text-foreground; }}Every element defaults to border-border and a semi-transparent focus outline. The body gets the semantic background and text colors.
Custom data-attribute variants
Section titled “Custom data-attribute variants”The stylesheet defines several custom variants for styling component states from shadcn-svelte and Bits UI:
| Variant | Matches |
|---|---|
data-open | [data-state="open"] or [data-open] |
data-closed | [data-state="closed"] or [data-closed] |
data-checked | [data-state="checked"] or [data-checked] |
data-unchecked | [data-state="unchecked"] or [data-unchecked] |
data-selected | [data-selected] |
data-disabled | [data-disabled="true"] or [data-disabled] |
data-active | [data-state="active"] or [data-active] |
data-horizontal | [data-orientation="horizontal"] |
data-vertical | [data-orientation="vertical"] |
Use them as Tailwind modifiers:
<div class="data-open:opacity-100 data-closed:opacity-0 transition-opacity"> ...</div>Custom utilities
Section titled “Custom utilities”no-scrollbar
Section titled “no-scrollbar”Hides the scrollbar while keeping scroll functionality:
@utility no-scrollbar { -ms-overflow-style: none; scrollbar-width: none; &::-webkit-scrollbar { display: none; }}<div class="overflow-y-auto no-scrollbar">...</div>The cn() utility
Section titled “The cn() utility”The cn() function in src/lib/utils.ts merges class names using clsx for conditional logic and tailwind-merge to deduplicate conflicting Tailwind classes:
import { clsx, type ClassValue } from "clsx";import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs));}Usage in components:
<script lang="ts"> import { cn } from "$lib/utils";
let { class: className, variant = "default" } = $props();</script>
<button class={cn( "rounded-lg px-4 py-2 font-medium", variant === "primary" && "bg-primary text-primary-foreground", variant === "outline" && "border border-input bg-background", className)}> <slot /></button>Dark / light mode
Section titled “Dark / light mode”Theme switching is handled by the mode-watcher package. It toggles the .dark class on the document root and persists the preference to localStorage.
<script> import { ModeWatcher } from "mode-watcher";</script>
<ModeWatcher /><!-- rest of layout -->To build a theme toggle, use the helpers from mode-watcher:
<script> import { toggleMode, mode } from "mode-watcher";</script>
<button onclick={toggleMode}> {$mode === "dark" ? "Light" : "Dark"}</button>Icon system
Section titled “Icon system”Honeycomb uses two icon libraries:
| Library | Package | Usage |
|---|---|---|
| Lucide | @lucide/svelte | Primary icon set — clean, consistent line icons |
| Tabler | @tabler/icons-svelte | Extended set for specialized icons |
<script> import { Home, Settings, Bell } from "@lucide/svelte"; import { IconBrandGithub } from "@tabler/icons-svelte";</script>
<Home class="size-5 text-muted-foreground" /><IconBrandGithub class="size-5" />Both libraries are tree-shakeable — only the icons you import are included in the bundle.
Scroll animations
Section titled “Scroll animations”The scrollAnimate Svelte action in src/lib/utils/scroll-animate.ts provides scroll-triggered animations using IntersectionObserver.
Animation types
Section titled “Animation types”| Type | Effect |
|---|---|
fade-up | Fade in + slide up (default) |
fade-down | Fade in + slide down |
fade-left | Fade in + slide from left |
fade-right | Fade in + slide from right |
scale | Fade in + scale from 92% |
blur | Fade in + deblur from 8px |
Basic usage
Section titled “Basic usage”<script> import { scrollAnimate } from "$lib/utils/scroll-animate";</script>
<div use:scrollAnimate>Fades up on scroll</div><div use:scrollAnimate={{ type: "scale", delay: 200 }}>Scales in after 200ms</div>Staggered children
Section titled “Staggered children”Pass stagger to animate child elements sequentially:
<ul use:scrollAnimate={{ type: "fade-up", stagger: 100 }}> <li>First (0ms)</li> <li>Second (100ms)</li> <li>Third (200ms)</li></ul>Options
Section titled “Options”| Option | Type | Default | Description |
|---|---|---|---|
type | AnimationType | "fade-up" | Animation style |
delay | number | 0 | Delay before animation starts (ms) |
duration | number | 700 | Animation duration (ms) |
threshold | number | 0.15 | IntersectionObserver visibility threshold |
stagger | number | — | Delay between each child (ms) |
once | boolean | true | Only animate once vs. every time element enters |
shadcn-svelte CLI
Section titled “shadcn-svelte CLI”The components.json at the project root configures the shadcn-svelte CLI:
{ "$schema": "https://shadcn-svelte.com/schema.json", "tailwind": { "css": "src/routes/layout.css", "baseColor": "slate" }, "aliases": { "components": "$lib/components", "utils": "$lib/utils", "ui": "$lib/components/ui", "hooks": "$lib/hooks", "lib": "$lib" }, "typescript": true, "registry": "https://shadcn-svelte.com/registry"}Add new UI primitives with:
npx shadcn-svelte@latest add buttonComponents are generated into $lib/components/ui/<name>/.