Theming
The application's CSS is the theme. Klean has no ThemeProvider, theme object, preset code, named theme catalog, or theme section in a configuration file.
Neutral component defaults work without global Klean tokens. Products apply their visual language with ordinary Tailwind, and the copied source remains replaceable.
Three levels of styling
One local treatment: use classes
<script setup>
import Button from '@/components/ui/button/Button.vue'
</script>
<template>
<Button class="bg-emerald-700 text-white hover:bg-emerald-800">
Approve invoice
</Button>
</template>
One repeated product concept: create a component
<!-- assets/js/components/DeployButton.vue -->
<script setup>
import Button from './ui/button/Button.vue'
</script>
<template>
<Button
class="min-h-11 bg-emerald-700 px-5 font-semibold text-white hover:bg-emerald-800"
>
<slot />
</Button>
</template>
This is the normal replacement for variant="primary". The product concept receives a product name and stays inside the product. Caller classes still merge last when the product component needs a local adjustment.
One shared value: use Tailwind's theme
@import 'tailwindcss';
@theme {
--color-brand: oklch(0.49 0.16 154);
--color-on-brand: oklch(0.985 0 0);
}<Button class="bg-brand text-on-brand hover:bg-brand/90">
Approve invoice
</Button>The token creates Tailwind utilities. It does not create component variants.
The Vue, React, and Svelte examples above all consume the same application CSS. Changing frameworks does not create a Klean theme layer.
Optional semantic foundations
Applications with coordinated modes or white-label branding can define a short set of application-owned roles such as canvas, ink, surface, muted, line, and focus. Product signals should have paired foregrounds such as brand / on-brand.
These roles are optional application infrastructure. Klean does not generate them, prompt for them, or require them before a component works.
Avoid component-specific tokens such as --button-primary-hover-background. They recreate a variant matrix in CSS and make the primitive harder to replace.
Light and dark
An application that follows the operating system can use Tailwind's ordinary dark: utilities. A manual switcher can expose one resolved mode on the root:
<html data-mode="dark"></html>@custom-variant dark (&:where([data-mode="dark"], [data-mode="dark"] *));If the stored preference is system, application-owned code resolves the media query and writes light or dark before paint. A server-readable cookie is the right convention when SSR must avoid a flash.
The mode preference is a Durable UI concern because a person chose it. Store that preference with the framework-native useStoredState or createStoredState source from the Durable UI bundle, resolve it before paint, and leave component source unaware of the storage mechanism.
Proving applications, not themes
Hagfish and Slipway intentionally keep different typography, spacing, color, density, shadow, and motion. They prove that Klean's semantic and behavioral source accepts radically different application-owned Tailwind. They are not named themes and do not appear in a theme selector.
Accessibility invariants
Every product mode must preserve:
- text, icon, boundary, and focus contrast;
- states communicated by more than color;
- readable disabled content;
- reduced-motion behavior independently of theme;
- native control rendering through the correct
color-scheme; - useful forced-colors behavior.
Klean does not guess contrast at runtime. Applications choose and test deliberate foreground/background pairs.
The decision rule
| Question | Use |
|---|---|
| Is this unique here? | Direct Tailwind classes |
| Is this a repeated product concept? | Application-owned component |
| Is this value shared across unrelated surfaces? | Tailwind @theme variable |
| Is this light/dark user state? | CSS plus one root mode attribute |
| Does this change behavior? | A behavioral prop |
Do not tokenize a value merely because it exists.