Welcome to Vanilla Breeze
This bell pulls live notifications from /go/notify/messages — the same contract documented at /docs/concepts/service-contracts/. Static articles like this one are the no-JS / no-backend fallback.
This bell pulls live notifications from /go/notify/messages — the same contract documented at /docs/concepts/service-contracts/. Static articles like this one are the no-JS / no-backend fallback.
A pack is a self-contained extension that adds capabilities or a visual identity to Vanilla Breeze. Packs come in three types:
Add JS behaviors and components. No theme tokens — they work with any theme. Examples: ui, effects, prototype.
Pure visual themes — token overrides for colors, fonts, radii, easing. No JS, no new components. CSS-only.
Theme + effects + components in one coherent package. A complete visual identity. Examples: retro, kawaii.
Vanilla Breeze core ships tokens, layouts, and web components. A pack extends this with:
Token overrides scoped to [data-theme~="name"]. Changes colors, fonts, radii, easing. Zero new CSS rules — just different values.
CSS and JS animations activated by data-* attributes. Add data-neon or data-flipboard to any element.
Web components with pack-specific chrome. They consume core tokens automatically — switch theme and the component adapts.
All three layers are optional. A pack can ship just a theme, just effects, just components, or any combination.
This is the key insight: pack components consume the same tokens as core. When you switch from data-theme="retro" to data-theme="swiss", or change the color accent via ThemeManager.setAccent('ocean'), every pack component re-renders with the new colors, fonts, and spacing. No code changes needed.
Load the full pack alongside core:
<!-- Load core Vanilla Breeze first --><link rel="stylesheet" href="/cdn/vanilla-breeze.css"><script type="module" src="/cdn/vanilla-breeze.js"></script> <!-- Load a pack (all-in-one) --><link rel="stylesheet" href="/cdn/packs/retro.full.css"><script type="module" src="/cdn/packs/retro.full.js"></script>
Load only the parts you need:
<!-- Theme tokens only (smallest payload) --><link rel="stylesheet" href="/cdn/packs/retro.theme.css"> <!-- Effects only (CSS data-* animations) --><link rel="stylesheet" href="/cdn/packs/retro.effects.css"><script type="module" src="/cdn/packs/retro.effects.js"></script> <!-- Components only (web components) --><script type="module" src="/cdn/packs/retro.components.js"></script>
Load a pack dynamically from JavaScript:
import { activateBundle } from '/cdn/vanilla-breeze.js' // Load a pack at runtime (returns a Promise)await activateBundle('retro') // Custom CDN pathawait activateBundle('retro', { basePath: '/assets/packs' })
activateBundle injects a <link> for the CSS and import()s the JS. It returns a Promise that resolves when both are loaded.
Vanilla Breeze uses CSS @layer to manage cascade priority. Pack layers sit after core layers, ensuring pack styles always win over core defaults without specificity battles:
@layer tokens, reset, native-elements, custom-elements, web-components, utils, bundle-theme, bundle-effects, bundle-components;
| Layer | Purpose | Owner |
|---|---|---|
tokens … utils |
Core Vanilla Breeze styles | Core |
bundle-theme |
Token overrides for [data-theme~="name"] |
Pack theme file |
bundle-effects |
CSS effects activated by data-* attributes |
Pack effects file |
bundle-components |
Web component styles (inside Shadow DOM) | Pack component files |
When no pack is loaded, the three bundle layers are empty declarations — zero cost.
An aesthetic pack lives in src/packs/{name}/ and follows a strict file convention:
src/packs/retro/ retro.theme.css # Token overrides (@layer bundle-theme) retro.effects.css # CSS data-* effects (@layer bundle-effects) retro.effects.js # JS effects (registerEffect calls) retro.components.js # Component registration (registerComponent calls) retro.bundle.js # Pack manifest metadata fonts/ # Pack-specific assets Cartridge-Regular.woff2 Cartridge-Bold.woff2 components/ audio-player/ audio-player.js # Web component class audio-player.contract.md
The build pipeline produces granular and combined outputs:
dist/cdn/packs/ manifest.json # Registry of all built packs retro.theme.css # Theme tokens only retro.effects.css # Effects CSS only retro.effects.js # Effects JS only retro.components.js # Components JS only retro.full.css # Theme + effects CSS combined retro.full.js # Effects + components JS combined retro.bundle.js # Pack manifest retro/fonts/ # Copied assets
The theme file overrides core tokens inside a @layer bundle-theme block, scoped by a [data-theme~="name"] selector:
/* Pack theme tokens scope to a data-theme selector */@layer bundle-theme { [data-theme~="retro"] { --color-primary: oklch(70% 0.28 145); --radius-m: 2px; --font-mono: 'Cartridge', 'VT323', monospace; --ease-out: steps(4); }}
Because it uses the ~= (word match) selector, users can combine themes: data-theme="retro dark".
CSS effects are data-* attribute selectors in the bundle-effects layer. They must:
:root pollution)var(--color-primary) so they adapt to theme changesprefers-reduced-motion and [data-motion-reduced] overrides@media print resets/* Effects use data-* attribute selectors in the effects layer */@layer bundle-effects { [data-neon] { --vb-neon-color: var(--color-primary); color: var(--vb-neon-color); animation: vb-neon-pulse 2.5s ease-in-out infinite alternate; } [data-neon="pink"] { --vb-neon-color: oklch(68% 0.3 340); } [data-neon="cyan"] { --vb-neon-color: oklch(72% 0.2 200); }}
JS effects use the registerEffect API. The pack registry provides a shared MutationObserver that auto-initializes effects on dynamically added elements:
import { registerEffect } from '/cdn/vanilla-breeze.js' registerEffect('flipboard', { selector: '[data-flipboard]', init(el) { /* set up split-flap animation */ }, destroy(el) { /* clean up timers and DOM */ }, reducedMotionFallback(el) { /* static display */ },})
| Property | Required | Description |
|---|---|---|
selector |
Yes | CSS selector to match (e.g. [data-flipboard]) |
init(el) |
Yes | Called when a matching element enters the DOM |
destroy(el) |
No | Called when a matching element is removed |
reducedMotionFallback(el) |
No | Called instead of init when motion is reduced |
Components use registerComponent for priority-based conflict resolution. Since customElements.define() is permanent, the registry uses first-wins semantics with priority tiebreaking:
import { registerComponent } from '/cdn/vanilla-breeze.js'import { MyWidget } from './components/my-widget/my-widget.js' registerComponent('my-widget', MyWidget, { bundle: 'my-pack', contract: 'my-widget', priority: 10,})
Every pack component documents which tokens it consumes and which it exposes. This makes the dependency chain explicit and enables tooling:
/* Components consume system tokens and expose their own */class MyWidget extends HTMLElement { static consumesTokens = ['--color-primary', '--color-surface-sunken'] static exposesTokens = ['--widget-accent', '--widget-bg']}
<!-- Default tokens from active theme --><audio-player src="track.mp3" data-title="Demo"></audio-player> <!-- Override tokens inline --><audio-player src="track.mp3" style="--audioplayer-screen-color: oklch(72% 0.3 340);"></audio-player> <!-- Mini form factor --><audio-player src="track.mp3" data-size="mini" data-title="Compact"></audio-player>
::part() API — expose named parts for external stylingvar(--color-primary) not oklch(70% 0.28 145)<audio> for JS-off fallbackbundle, contract, version, token arraysTry the packs in the interactive explorer, or load them individually in your projects.
Add capabilities beyond core. Work with any theme.
Theme picker and environment manager for broad theme switching UIs.
Type: functional
Text effects, animated images, ticker counters, and star ratings.
Type: functional
Placeholder content for rapid prototyping and wireframing.
Type: functional
Standalone modules loaded separately: data-emoji, <emoji-picker>, extended emoji set.
Type: functional
Opt-in variable font bundles. One file per typographic role — no multi-weight loading.
Inter (sans/UI), Literata (serif/editorial), Recursive (mono/code). Variable fonts with axis-aware tokens.
Type: font pack (~660KB Latin subset)
Fraunces (WONK/SOFT axes), Cormorant (Garamond-lineage), Bodoni Moda (dramatic contrast). For editorial and marketing.
Type: font pack (~430KB Latin subset)
Nabla (COLR v1 3D depth), Honk (inflatable neon), Kablammo (comic impact). For hero sections and creative contexts.
Type: font pack (~610KB)
Material Symbols Outlined variable icon font with attractor animations. Context-aware weight and optical size.
Type: icon pack (theme + effects + JS)
Theme + effects + components — a complete visual identity in one package.
Phosphor green, sharp corners, Cartridge font, CRT effects, split-flap board, audio player with oscilloscope.
Type: full (theme + effects + components)
Pastel pink/mint/lavender palette, pill shapes, bouncy motion, Cherry Bomb One display font, sparkle particles.
Type: full (theme + effects + components)
Bold flat colour, geometric surface patterns (stripes, dots, zigzag, squiggle, confetti), hard drop shadows, Boogaloo + Outfit + Space Mono fonts.
Type: full (theme + effects)
Interactive demo page — load and switch between all available packs live.
Try packs live in the interactive demo.
Understand the progressive enhancement philosophy.
Learn about the core token system that packs extend.