Klean UI means Kelvin's Lean UI: source-owned, accessible UI for The Boring JavaScript Stack. It gives Vue, React, and Svelte applications framework-native components with neutral defaults, ordinary Tailwind styling, and Durable UI patterns. The source belongs to the application as soon as it is added.
Run the same command in every standard Boring Stack application:
npx klean-ui add button
npx klean-ui add input
npx klean-ui add textarea
npx klean-ui add rich-text
npx klean-ui add checkbox
npx klean-ui add radio
npx klean-ui add switch
npx klean-ui add separator
npx klean-ui add popover
npx klean-ui add menu
npx klean-ui add select
npx klean-ui add dialog
npx klean-ui add toast
npx klean-ui add durable-uiKlean detects Sails and the frontend framework from package.json and the conventional application entry. It then installs the selected framework-native registry item and its prerequisites. Button is one source file:
Vue assets/js/components/ui/button/Button.vue
React assets/js/components/ui/button/Button.jsx
Svelte assets/js/components/ui/button/Button.svelte
Multi-file components remain conventional. Toast installs the detected renderer and one framework-neutral controller beside it:
Vue assets/js/components/ui/toast/Toast.vue
React assets/js/components/ui/toast/Toast.jsx
Svelte assets/js/components/ui/toast/Toast.svelte
All assets/js/components/ui/toast/toast.js
There is no init, klean-ui.json, framework questionnaire, alias questionnaire, generated cn.js, or Klean runtime dependency. Each installed component is ordinary application code. Its visual API is class in Vue and Svelte or className in React, and caller Tailwind classes win.
Preview the resolved work without changing the application:
npx klean-ui add button --dry-runNonstandard Boring Stack applications can use explicit path overrides:
npx klean-ui add button \
--components-dir assets/js/design-system \
--css assets/styles/app.cssKlean will not silently replace edited source. Re-running an unchanged installation is a no-op; edited files produce a useful conflict and require the deliberate --overwrite flag.
Read the complete installation guide.
Install the project-level skill from the Klean UI repository:
npx skills add sailscastshq/klean-ui --skill klean-uiThen invoke $klean-ui when building, migrating, or reviewing an interface. The
skill teaches the agent Klean's source-ownership, Tailwind, native-platform,
accessibility, and Durable UI decisions; the canonical component APIs remain in
the documentation. Add -g to the installation command only when you want the
skill available across projects.
Durable UI is Klean source, not a second runtime package. One zero-configuration command adds the framework-native utilities and their small browser core:
npx klean-ui add durable-uiVue and React receive idiomatic useStoredState, useQueryState, useFormDraft, useWizardDraft, useScrollRestoration, useOptimistic, and useSearch APIs. Svelte receives the equivalent create… rune APIs. Defaults are SSR-safe and use namespaced, versioned storage; URL defaults stay out of the address; drafts expire and restore deliberately; scroll is session-scoped; optimistic work rolls back; and stale searches are aborted. No provider, initializer, manifest, or Klean state runtime is introduced.
Copied source still belongs to the application. Klean therefore inspects before it writes:
npx klean-ui check
npx klean-ui diff button
npx klean-ui update button
npx klean-ui update --allcheck distinguishes current source, safely replaceable historical Klean source, local edits, and untracked files. diff is read-only. update replaces only an exact known Klean revision and applies component files, direct dependency changes, and lockfile changes as one rollback-safe transaction. Locally edited or unknown source is never replaced unless the caller deliberately passes --overwrite.
No application manifest is created. Revision hashes and migration notes stay inside the versioned CLI registry. Read the update guide.
Klean installs Input and Textarea independently. The application writes the real label, help, and error elements so the form remains obvious HTML instead of a configuration API.
<div class="grid gap-2">
<label for="email">Email address</label>
<Input
id="email"
v-model="form.email"
name="email"
type="email"
autocomplete="email"
required
:aria-invalid="Boolean(form.errors.email)"
aria-describedby="email-help email-error"
/>
<p id="email-help">We only use this for account messages.</p>
<p id="email-error" class="empty:hidden text-sm text-red-700">
{{ form.errors.email }}
</p>
</div>aria-invalid="false" is valid, and the stable empty error contributes no description. When an error appears, the same relationship becomes useful without conditional IDs or a Klean helper. Validation, IDs, messages, and values remain application-owned. Input and Textarea forward native attributes, while visual density, color, shape, and layout remain ordinary caller Tailwind classes. Read the canonical Input documentation.
Button has behavioral inputs only: rendered element, native button type, and disabled semantics. Actions remain buttons; navigation renders a native anchor or the Boring Stack/Inertia Link component. Loading labels and product styling remain application concerns.
<script setup>
import Button from "@/components/ui/button/Button.vue";
</script>
<template>
<Button
type="submit"
:disabled="form.processing"
class="rounded-full bg-emerald-700 px-6 hover:bg-emerald-800"
>
{{ form.processing ? "Saving" : "Save changes" }}
</Button>
</template>There is intentionally no variant, size, tone, color, radius, or generated class-helper API. Repeated visual treatments become application-owned components styled with Tailwind.
npm install
npm run storybookStorybook opens one neutral workbench at http://localhost:6006. Its sidebar
composes three framework-native Storybooks:
6006 Klean UI workbench
6007 Vue
6008 React
6009 Svelte
Open Vue, React, or Svelte, then choose Components / Button, Input, Textarea, Checkbox, Radio, Switch, Spinner, Tooltip, Tabs, Popover, Menu, Select, Combobox, Command, Dialog, Separator, Slide, or Toast. The stories mount the actual registry source and share behavior expectations without sharing a runtime component abstraction. Controls stay limited to useful behavioral inputs; interaction stories and the accessibility addon verify the durable contract.
npm run build-storybook produces the same composition as one static artifact:
the neutral shell at storybook-static, with each renderer nested below it.
npm test
npm run build
npm run build-storybook
npm pack --dry-runThe package contains the CLI and its versioned registry. It does not publish the Storybook, development build, or framework runtimes.
Klean is the canonical implementation of our Durable UI practice. Durable behavior lives in the component, composable, or Boring Stack block that owns it; it does not turn every primitive into a state-management abstraction.
The canonical documentation lives at docs.sailscasts.com/klean-ui, including the doctrine, installation guide, Durable UI contract, theming convention, and every component page.
npx klean-ui add flag installs framework-native Flag and its local asset
registry. country accepts a case-insensitive two-letter code (surrounding
whitespace is ignored). A nonempty src takes precedence. Omitted alt derives a stable English
country name: <Flag country="NG" /> has alt="Nigeria". An explicit string
always wins, including alt="" for decorative flags beside country text or
alt="Based in Nigeria" for application meaning. alt remains a string,
not a Boolean switch. Native image attributes and error listeners pass through. Missing,
invalid or failed images render a fallback span, hidden from assistive
technology when decorative, named by alt when informative. Vue/Svelte slots
or React children own fallback content. Changing the resolved source retries.
<Flag country="NG" class="w-5" />
<Flag country="KE" class="w-8" />
<Flag country="NG" alt="Based in Nigeria" />
<Flag country="NG" alt="" />
<!-- Decorative beside country text -->
<Flag country="GH" class="size-10 aspect-square rounded-full" />
<Flag src="/flags/organization.svg" alt="Organization" class="w-12" />Default geometry is 24px wide, 3:2. Use w-5, w-6, w-8, w-10, w-12
for 20/24/32/40/48px. A circle uses aspect-square rounded-full and a width or
size class. Klean deliberately has no size or circle presentation props.
React uses className; Svelte uses class.
Original component behavior; no Flux implementation source is used. Flag art
comes from country-flag-icons
1.6.20, npm tarball SHA-1 aa6f36104568993f9cd43e7da283f9b7d0a802cc.
The full MIT license travels in flags.js, including installed application
source. The fixed registry has 257 entries: 249 ISO 3166-1 codes plus AC, EU,
IC, TA, XA, XC, XK, XO supplied by this asset version. This is asset coverage,
not an assertion of political status or a Unicode RGI registry. countries
is exported from the installed flags.js; applications own the eligible
picker list and localized country names. countryName(country) exports the
fixed English lookup. Names are checked in alongside the asset registry, with
no render-time Intl.DisplayNames, browser language lookup, or locale prop.
The same code yields the same default name during SSR and hydration. Override
alt with the application's localized label. Unknown or missing codes derive
""; a custom source without a known country must receive a meaningful
explicit alt or alt="" for deliberate decoration. Any nonempty custom src disables inferred naming even when country is
also supplied: the source could depict anything. Only an explicit alt
labels a custom image.
XA/XC/XO names follow this asset package (Abkhazia/Northern Cyprus/South
Ossetia), not Unicode's unrelated pseudo-region assignments.
SVGs are encoded as data URLs in one application-owned module (~247KB before
minification/compression), avoiding asset-host setup and all built-in flag
network requests. The complete registry is eagerly included when Flag is
imported. Lazy-load a country picker where appropriate; a product using only
one flag can use a local src and trim its owned registry. Allow data: in
CSP img-src. No asset dependency or tracker runs at render time. A custom
src follows the application's network/CSP/privacy policy. Flag depicts an
explicit chosen region; never infer ethnicity or nationality from identity.