kawaii-ui
✨ early preview

kawaii-ui

A framework-agnostic kawaii / Y2K component library. Drop <bloom-*> into Angular, HTMX, React, Vue, or plain HTML — built on Lit, themed through CSS custom properties, zero lock-in.

📦 npm install Star on GitHub
Primary Secondary Accent Ghost Danger
<bloom-button variant="primary">Primary</bloom-button>
Lit 3 Web Components TypeScript MIT 5.1 KB gz
Why

Built for every stack

Each component is a standards-based custom element. Upgrade happens natively, which means the same <bloom-button> works in Angular templates, HTMX fragments, and plain HTML pages with zero glue code.

💖
Kawaii by default
Gel buttons, glossy surfaces, pastel gradients, glow shadows — the whole Y2K aesthetic, production-ready.
🧩
Framework-agnostic
Lit under the hood, standard custom elements on the outside. Works in Angular, React, Vue, HTMX, or none of the above.
🎨
Themed with CSS vars
Every color, radius, shadow, and transition is a CSS custom property. Override at any scope — no build step, no override props.
Accessible core
Real <button>s, form-associated elements, focus-visible rings, reduced-motion respect, proper ARIA.
Setup

Install

Pick whichever path matches your project. All three load the same artifact under the hood.

npm (Angular, React, Vite, Webpack…)
// main.ts — import once, use anywhere
import '@hazeliscoding/kawaii-ui';
import '@hazeliscoding/kawaii-ui/styles/tokens.css';
CDN (HTMX, vanilla, static sites)
<link rel="stylesheet" href="https://esm.sh/@hazeliscoding/kawaii-ui/dist/styles/tokens.css">
<script type="module" src="https://esm.sh/@hazeliscoding/kawaii-ui"></script>
bloom-button

Variants

Five semantic variants covering the typical kawaii/Y2K palette. Each uses a gel gradient with an inner highlight and a colored glow shadow.

Variants
Primary Secondary Accent Ghost Danger
<bloom-button variant="primary">Primary</bloom-button>
<bloom-button variant="secondary">Secondary</bloom-button>
<bloom-button variant="accent">Accent</bloom-button>
<bloom-button variant="ghost">Ghost</bloom-button>
<bloom-button variant="danger">Danger</bloom-button>
bloom-button

Sizes

Three sizes — sm, md, and lg — with consistent padding, corner radius, and minimum height.

Sizes
Small Medium Large
<bloom-button size="sm">Small</bloom-button>
<bloom-button size="md">Medium</bloom-button>
<bloom-button size="lg">Large</bloom-button>
bloom-button

States

Disabled, loading, and full-width modifiers. Loading preserves the button's width so layout doesn't jump.

States
Default Disabled Loading
<bloom-button>Default</bloom-button>
<bloom-button disabled>Disabled</bloom-button>
<bloom-button loading>Loading</bloom-button>
Full-width
Full-width accent
<bloom-button variant="accent" full-width>Full-width accent</bloom-button>
bloom-button

Icons

Two named slots — icon-left and icon-right — accept any HTML: emoji, SVG, icon-font spans. They're hidden when empty, so no stray padding.

Icon slots
💖 Like Share Favorite
<bloom-button variant="primary">
  <span slot="icon-left">💖</span>
  Like
</bloom-button>
bloom-button

Events

Clicks dispatch a composed bloom-click CustomEvent. The original MouseEvent is available at event.detail.originalEvent.

Live counter
Click me Clicks: 0
<bloom-button id="cta">Click me</bloom-button>
<script>
  cta.addEventListener('bloom-click', (e) => {
    console.log(e.detail.originalEvent);
  });
</script>
bloom-button

Forms

bloom-button is a form-associated custom element. type="submit" triggers native form submission, type="reset" resets — just like the built-in button.

Submit / reset
Submit Reset
Form not submitted yet
<form>
  <input name="name" required />
  <bloom-button type="submit" variant="primary">Submit</bloom-button>
  <bloom-button type="reset" variant="ghost">Reset</bloom-button>
</form>
bloom-badge

Badges

Pill-shaped status tags in six kawaii pastel colors. Optional dot indicator pulses gently for live/active states.

Colors
Pink Blue Green Lilac Yellow Neutral
<bloom-badge color="pink">Pink</bloom-badge>
<bloom-badge color="lilac">Lilac</bloom-badge>
With status dot
Live Trending New Beta Archived
<bloom-badge color="green" dot>Live</bloom-badge>
Sizes
Small Medium
bloom-avatar

Avatars

Circular avatars with a decorative outer ring, automatic initials fallback, and optional status dots. The watching status gently pulses pink.

Sizes
<bloom-avatar size="lg" name="Cosmo Bloom"></bloom-avatar>
With status
<bloom-avatar status="online" name="Hazel"></bloom-avatar>
<bloom-avatar status="watching" name="Luna"></bloom-avatar>
bloom-avatar-stack

Avatar stack

Group avatars with a subtle horizontal overlap — hover any one to lift it forward.

Watch party members
<bloom-avatar-stack aria-label="Watch party">
  <bloom-avatar size="sm" name="Bubble" status="online"></bloom-avatar>
  <bloom-avatar size="sm" name="Peach" status="watching"></bloom-avatar>
</bloom-avatar-stack>
bloom-card

Cards

Content containers with a soft hover lift and optional highlighted variant that adds an animated rainbow sparkle border. Named header and footer slots.

Default + highlighted

Default card

A standard card with soft border and hover lift.

View more

Highlighted

Animated rainbow sparkle border for emphasis.

Explore

Cozy Movie Night

Picking tonight's feature with your crew. Rated 9/10 so far!

Live
<bloom-card variant="highlighted">
  <h3 slot="header">Highlighted</h3>
  <p>Body content</p>
  <div slot="footer">
    <bloom-button size="sm">Explore</bloom-button>
  </div>
</bloom-card>
bloom-input

Inputs

Form-associated text inputs with labels, hints, error states, and prefix/suffix slots. Participates in native <form> submission and reset, just like bloom-button.

Labels & hints
<bloom-input label="Email" type="email" required></bloom-input>
<bloom-input label="Name" error="Required!"></bloom-input>
With prefix / suffix slots
🔍 @ ¥ / mo
Sizes
bloom-modal

Modals

Overlay dialog with focus trap, Escape handling, backdrop click, and body scroll lock. The consumer owns the open attribute — the modal dispatches bloom-close with a reason.

Trigger & dialog
💬 Open modal
<bloom-button @click="modal.open = true">Open</bloom-button>

<bloom-modal #modal @bloom-close="modal.open = false">
  <h3 slot="header">Start a watch party</h3>
  <p>Body content…</p>
  <div slot="footer">
    <bloom-button variant="primary">Continue</bloom-button>
  </div>
</bloom-modal>

Start a watch party

Pick a friend, pick a night, let fate pick the rest. The modal traps focus inside itself — try Tab and Shift+Tab. Press Escape to close.

Cancel Send invite
bloom-compat-ring

Compatibility rings

Animated SVG score rings for 0–100 values. Stroke color crosses thresholds automatically: pink under 50, yellow to 79, lime green at 80+.

Threshold colors
<bloom-compat-ring
  score="92"
  label="Kindred spirits"
  context="Based on 18 shared ratings"
></bloom-compat-ring>
Empty state
// Omit score — custom empty copy via named slot
<bloom-compat-ring>
  <p slot="empty">Rate 3 more together!</p>
</bloom-compat-ring>
Compositions

Real-world combos

Components working together — the whole point of a design system.

Space, search, profile
Our Watch Party

3 in the queue · 12 finished

Cozy Dreamy
Open Invite

Find shows

🔍
Search
Hazel Online

42 shows finished · 8.2 avg rating

View Settings
Theming

Playground

Swap gradient tokens at any scope — container, component, or :root. Click a palette below to retheme these four buttons without rebuilding anything.

Live theme swap
Primary Secondary Accent Danger
/* Theme scoped to one container — no props, no rebuild */
.sunset {
  --bloom-gradient-gel-pink: linear-gradient(180deg, #ffe29a, #ff6b9d, #c04e8c);
  --bloom-pink-600: #c04e8c;
}
Theming

Dark mode

Set data-theme="dark" on any ancestor (<html>, <main>, or even a single section). Tokens swap automatically.

Scoped dark block
Primary Secondary Accent Ghost
<section data-theme="dark">
  <bloom-button variant="primary">Primary</bloom-button>
</section>
bloom-theme-toggle

Theme toggle

A drop-in sun / moon toggle. On click it flips data-theme on <html>, persists to localStorage, and fires a bloom-theme-change event. Respects prefers-color-scheme on first load.

Drop-in
Click me — I control the whole page.
<bloom-theme-toggle></bloom-theme-toggle>

// Listen for theme changes
toggle.addEventListener('bloom-theme-change', (e) => {
  console.log(e.detail.theme); // "dark" | "light"
});