@anguszhu/kroma-ui
Kroma
A small, themeable component library — buttons, dialogs, toasts, switches, selects, and a few other primitives, extracted from a real production app and generalized for reuse. Ships idiomatic Vue 3 and React implementations from the same package, sharing CSS and framework-agnostic logic (focus trapping, dismiss timers) so the two stay in lockstep.
- Vue 3
<script setup>, or React 18+/19 — both fully typed, both idiomatic to their framework (v-modelin Vue, controlled props in React) rather than a lowest-common-denominator API. - Themeable by design — every color, spacing and radius is a CSS custom
property (
--kr-*). Override any of them at:rootor on a wrapper element to reskin the whole library; nothing is hardcoded. The same stylesheet styles both frameworks identically. - Light/dark for free via native CSS
light-dark()— components followcolor-scheme, no dark-mode class toggling required. - No icon font dependency. Components that need an icon (search, clear,
toast status) ship a small inline SVG default and expose a slot (Vue) or a
ReactNodeprop (React) to replace it with your own. - Tree-shakeable — ESM + CJS builds, each component is a separate export.
- Minimal dependencies —
vueandreact/react-domare the only peer dependencies, and each is optional: installing Kroma for a React project never pulls in Vue, and vice versa.
Requirements
Kroma's default theme uses light-dark() and color-mix(), so it targets
evergreen browsers (Chrome 123+, Safari 17.5+, Firefox 120+). vue (^3.4.0)
and react/react-dom (^18.0.0 || ^19.0.0) are both optional peer
dependencies — install whichever framework you're using.
Install
# Vue
npm install @anguszhu/kroma-ui vue
# React
npm install @anguszhu/kroma-ui react react-dom
Usage
Import the stylesheet once (usually in your app's entry point) — it's shared by both frameworks:
import '@anguszhu/kroma-ui/style.css'
Vue
<script setup lang="ts">
import { ref } from 'vue'
import { KrButton, KrDialog, KrTextField } from '@anguszhu/kroma-ui/vue'
const showDialog = ref(false)
const name = ref('')
</script>
<template>
<KrButton @click="showDialog = true">Rename</KrButton>
<KrDialog
v-model:show="showDialog"
title="Rename bookmark"
:actions="[{ text: 'Save', action: () => save() }]"
>
<KrTextField v-model="name" label="Name" autofocus />
</KrDialog>
</template>
React
import { useState } from 'react'
import { KrButton, KrDialog, KrTextField } from '@anguszhu/kroma-ui/react'
export function RenameButton() {
const [showDialog, setShowDialog] = useState(false)
const [name, setName] = useState('')
return (
<>
<KrButton onClick={() => setShowDialog(true)}>Rename</KrButton>
<KrDialog
show={showDialog}
onShowChange={setShowDialog}
title="Rename bookmark"
actions={[{ text: 'Save', action: () => save() }]}
>
<KrTextField value={name} onValueChange={setName} label="Name" autofocus />
</KrDialog>
</>
)
}
Prop names carry the same meaning across both frameworks, translated to each
framework's own idiom: Vue's v-model:show / @update:show pairs with
React's show / onShowChange; Vue's provide/inject (used internally by
KrSelectGroup) pairs with a React Context. Component and prop names
otherwise match 1:1.
Components
| Component | Description |
|---|---|
KrButton |
primary / secondary / link button variants. |
KrOverlay |
Fixed-position scrim + content slot with enter/leave transitions. Building block for dialogs, menus and popovers. |
KrDialog |
Modal dialog built on KrOverlay — title, body text, a default slot for arbitrary form content, and an actions array. Handles Escape-to-close and Tab focus trapping. |
KrSection |
A titled card/section wrapper. |
KrSelectGroup / KrSelectItem |
Single-select radio group with default / color / icon / card / text visual variants, and shapes square / circle / rounded. Use KrSelectItem standalone or let the group render items from a data array. |
KrSwitch |
Visual toggle switch (presentational — pair it with a native checkbox, the way KrToggleRow does, for accessible standalone use). |
KrToggleRow |
An accessible label + native checkbox + KrSwitch, with optional title/subtitle. |
KrTextField |
Labeled text input with an error state (shake animation + red outline). |
KrSearchInput |
Pill-shaped search input with a clear button; label / clearLabel props for i18n. |
KrToast |
Auto-dismissing status toast with an optional action button and countdown. |
KrEmpty |
Empty-state placeholder: icon slot, title, description. |
KrMenuItem |
A single menu row (or type="divider"), with an optional icon slot/prop. |
KrChevron |
A tiny CSS-only directional triangle, handy as a disclosure indicator. |
Every component's props and emitted events are typed — import the exported
Kr* types (e.g. KrSelectItemData, KrDialogAction) from @anguszhu/kroma-ui/vue or
@anguszhu/kroma-ui/react alongside the components for full autocomplete.
Theming
Everything reads from a small set of semantic tokens defined in
@anguszhu/kroma-ui/style.css (see src/styles/tokens.css in this repo). To restyle,
override the tokens you care about — no build step, no config:
:root {
--kr-theme: #7c3aed; /* your brand accent */
--kr-theme-secondary: #a78bfa;
--kr-surface: #ffffff;
--kr-spacing-3: 20px;
--kr-font-family: "Inter", -apple-system, system-ui, sans-serif;
}
Every component sets its own font-family: var(--kr-font-family) rather than
relying on inheriting whatever font the host page happens to have in scope at
that point in the DOM — the default is a system-font stack, so components
look right out of the box even before you override it.
Because the accent tokens are defined with light-dark(), you can supply a
dark-mode value in the same declaration: --kr-theme: light-dark(#7c3aed, #c4b5fd);. Pair that with color-scheme: light dark (already set by the
[data-kr-color-scheme="auto"] selector Kroma ships) to switch automatically
with the OS.
Development
npm install
npm run test # vitest — core logic + every component, both frameworks
npm run typecheck # vue-tsc (src/vue + src/core) and tsc (src/react + src/core)
npm run build # type declarations + ESM/CJS bundles for both entries + style.css
npm run storybook:vue # preview the Vue components at http://localhost:6006
npm run storybook:react # preview the React components at http://localhost:6007
Source is split into src/core (framework-agnostic logic and types, not
published on its own), src/vue, and src/react — each component gets an
idiomatic, hand-authored implementation per framework, sharing one CSS file
per component (src/styles/components/) so the two can never visually drift
apart. Each component has a colocated *.stories.ts/*.stories.tsx file.
Storybook itself, @storybook/*, .storybook*/, and *.stories.* files are
dev-only tooling — package.json's files field publishes only dist/, so
none of it ships to npm. npm run build-storybook:vue /
npm run build-storybook:react produce static storybook-static/ sites if
you want to deploy a hosted preview separately.
What's not in here
This package intentionally ships only generic, product-agnostic UI. Anything tied to a specific app's domain model (e.g. a bookmark tree, browser-specific favicon fetching) stays in that app rather than leaking into a shared component library.
Related Packages
Cluster Envision reusable Web Components built on top of the CE UI framework.
Cluster Envision reusable Web Components built on top of the CE UI framework.
Arcana Labs UI Components — a typed, framework-native shadcn-style component lib...
Version History
| Version | Released | Vue | License |
|---|---|---|---|
| 0.2.1 | ^3.4.0 | MIT | |
| 0.2.0 | ^3.4.0 | MIT | |
| 0.1.0 | ^3.4.0 | MIT |