Why Flavors, Not Themes β
The Problem with Traditional Theming β
Most design systems use generic approaches like data-theme
attributes or framework-specific <ThemeProvider>
wrappers. While functional, these approaches have significant limitations:
1. All-or-Nothing Theming β
<!-- β Traditional approach: data-theme -->
<html data-theme="dark">
<body>
<!-- ALL components must be dark -->
<button>Dark button</button>
<button>Dark button</button>
<button>Dark button</button>
<!-- Want ONE light button? Need a wrapper div -->
<div data-theme="light">
<button>Light button (requires wrapper π)</button>
</div>
</body>
</html>
Problems:
- β Requires extra wrapper elements for individual overrides
- β Cannot mix themes easily in the same context
- β Creates unnecessary DOM nesting
- β Harder to maintain and reason about
2. Framework Lock-In β
// β React-only approach
import { ThemeProvider } from '@my-design-system/react';
function App() {
return (
<ThemeProvider theme="dark">
<Button>Only works in React</Button>
</ThemeProvider>
);
}
Problems:
- β Only works in specific frameworks (React, Vue, Angular)
- β Breaks the promise of Web Components (framework-agnostic)
- β Requires different implementations for each framework
- β Users stuck with your framework choice
3. Lack of Granularity β
<!-- β Context-only theming -->
<div class="theme-dark">
<button>I'm dark</button>
<button>I'm dark</button>
<!-- I want THIS button to be accent color, but... -->
<button class="theme-accent">Still inherits dark theme</button>
</div>
Problems:
- β Hard to create exceptions or highlights
- β Requires complex CSS overrides
- β Not intuitive for developers
The Sando Solution: Flavors π₯ͺ β
Inspired by the Japanese "katsu sando" (sandwich), Sando uses a flavor-based system that treats theming like sandwich fillings: you can choose different flavors for different parts, mix them freely, and create unique combinations.
Core Philosophy β
"Every component can have its own flavor, while respecting inherited context"
Think of it like a sandwich shop:
- π Ingredients (bottom bread): Raw materials (colors, spacing)
- π₯ Flavors (filling): The theme/taste (original, strawberry, chocolate, dark)
- π Recipes (top bread): Final component styling
How It Works β
<!-- β
Sando approach: flavor attribute -->
<html flavor="dark">
<body>
<!-- Components inherit dark flavor -->
<sando-button>Dark button (inherited)</sando-button>
<sando-button>Dark button (inherited)</sando-button>
<!-- Override individual component - NO wrapper needed! -->
<sando-button flavor="strawberry">Strawberry button</sando-button>
<!-- Back to dark -->
<sando-button>Dark button (inherited)</sando-button>
</body>
</html>
Benefits:
- β Granular control: Change individual components
- β No wrappers: Direct attribute on component
- β Mix and match: Different flavors in same context
- β Framework-agnostic: Works everywhere (React, Vue, Angular, Vanilla)
- β Intuitive: "This button tastes like strawberry"
Real-World Use Cases β
1. E-commerce: Featured Products β
<div flavor="dark" class="product-grid">
<!-- Regular products use dark flavor -->
<product-card>
<h3>Regular Product</h3>
<sando-button>Add to Cart</sando-button>
</product-card>
<product-card>
<h3>Regular Product</h3>
<sando-button>Add to Cart</sando-button>
</product-card>
<!-- Featured product stands out with different flavor -->
<product-card class="featured">
<h3>β FEATURED</h3>
<sando-button flavor="gold">Buy Now - 50% OFF!</sando-button>
</product-card>
<product-card>
<h3>Regular Product</h3>
<sando-button>Add to Cart</sando-button>
</product-card>
</div>
Why it works: The featured CTA stands out without changing the entire card's theme.
2. SaaS Dashboard: Contextual Actions β
<div flavor="light" class="dashboard">
<nav>
<sando-button variant="ghost">Home</sando-button>
<sando-button variant="ghost">Analytics</sando-button>
<sando-button variant="ghost">Settings</sando-button>
</nav>
<main>
<h1>Welcome back, User!</h1>
<!-- Danger actions use destructive flavor -->
<sando-button flavor="destructive" status="destructive">
Delete Account
</sando-button>
<!-- Primary actions use vibrant flavor -->
<sando-button flavor="vibrant">
Upgrade to Pro
</sando-button>
</main>
</div>
Why it works: Different actions communicate different intents through flavor.
3. Marketing Landing Page: Section Themes β
<!DOCTYPE html>
<html flavor="light">
<!-- Hero section: light flavor -->
<section class="hero">
<h1>Welcome to Our Product</h1>
<sando-button size="large">Get Started</sando-button>
</section>
<!-- Features section: changes to dark flavor -->
<section flavor="dark" class="features">
<h2>Amazing Features</h2>
<sando-button variant="outline">Learn More</sando-button>
</section>
<!-- Testimonials: back to light (inherited from html) -->
<section class="testimonials">
<h2>What Customers Say</h2>
<sando-button variant="ghost">Read Reviews</sando-button>
</section>
<!-- CTA section: vibrant flavor for conversion -->
<section flavor="vibrant" class="cta">
<h2>Ready to Start?</h2>
<!-- This button inherits vibrant, but we override it -->
<sando-button flavor="strawberry" size="large">
Start Free Trial
</sando-button>
</section>
</html>
Why it works: Each section has its own flavor, components inherit automatically, and individual overrides are still possible.
Flavor Inheritance β
Flavors follow a hierarchical inheritance model:
<html flavor="dark"> <!-- Level 1: Global -->
<body>
<div flavor="ocean"> <!-- Level 2: Section -->
<article> <!-- Level 3: Inherits ocean -->
<!-- Inherits ocean from ancestor -->
<sando-button>Ocean button</sando-button>
<!-- Override to strawberry -->
<sando-button flavor="strawberry">
Strawberry button
</sando-button>
<!-- Back to ocean -->
<sando-button>Ocean button</sando-button>
</article>
</div>
<!-- Outside the ocean section, back to dark -->
<sando-button>Dark button</sando-button>
</body>
</html>
Inheritance Rules:
- Component looks for explicit
flavor
attribute on itself - If none, looks for nearest ancestor with
flavor
attribute - If none found, uses default
flavor="original"
Comparison Table β
Feature | Traditional data-theme | Strapi ThemeProvider | Sando flavor |
---|---|---|---|
Framework Agnostic | β Yes | β React-only | β Yes |
Individual Overrides | β οΈ Requires wrapper | β οΈ Requires nested provider | β Direct attribute |
Mix Themes | β Difficult | β Difficult | β Natural |
Performance | β Good | β οΈ Re-renders | β Excellent |
Intuitive API | β οΈ Generic | β Framework-native | β Flavor analogy |
Type Safety | β No | β Yes | β οΈ Partial (via TS) |
DOM Overhead | β οΈ Extra wrappers | β οΈ Extra providers | β None |
Unique Philosophy | β Generic | β Common | β Sandwich analogy |
The "Katsu Sando" Philosophy β
What is a Katsu Sando? β
A katsu sando is a Japanese sandwich consisting of:
- Bread (top and bottom)
- Katsu (breaded cutlet - the star)
- Sauce & fixings
How it Maps to Sando Design System β
βββββββββββββββββββββββ
β Top Bread β β Recipes (Component tokens)
βββββββββββββββββββββββ€
β Katsu + Sauce β β Flavors (Semantic/theme tokens)
βββββββββββββββββββββββ€
β Bottom Bread β β Ingredients (Primitive tokens)
βββββββββββββββββββββββ
The Analogy β
- Ingredients = Basic building blocks (flour, oil, eggs)
- Flavors = The taste/theme (tonkatsu sauce, curry, strawberry jam)
- Recipes = Final assembled dish (button, card, modal)
Just like you can make:
- Original katsu sando (classic tonkatsu sauce)
- Strawberry sando (sweet fruit filling)
- Chocolate sando (dessert version)
You can create:
flavor="original"
(default theme)flavor="strawberry"
(pink/red theme)flavor="chocolate"
(brown theme)
The key insight: Different flavors, same structure. Different themes, same architecture.
Technical Implementation β
How Flavors Work Under the Hood β
// 1. Component has flavor property
@customElement('sando-button')
export class SandoButton extends LitElement {
@property({ reflect: true })
flavor = 'original';
connectedCallback() {
super.connectedCallback();
// If no explicit flavor, inherit from ancestor
if (!this.hasAttribute('flavor')) {
const inherited = this.closest('[flavor]')?.getAttribute('flavor');
if (inherited) {
this.flavor = inherited;
}
}
}
}
/* 2. CSS selects based on flavor attribute */
:host([flavor="original"]) {
--button-bg: var(--color-action-solid-background-default);
--button-text: var(--color-action-solid-text-default);
}
:host([flavor="strawberry"]) {
--button-bg: #ff6b6b;
--button-text: white;
}
:host([flavor="dark"]) {
--button-bg: #1f2937;
--button-text: white;
}
/* 3. Component styles use the CSS variables */
.button {
background: var(--button-bg);
color: var(--button-text);
/* ... */
}
CSS Custom Properties for Overrides β
<!-- Global flavor -->
<sando-button flavor="original">Original</sando-button>
<!-- Surgical override via CSS variables -->
<sando-button
flavor="original"
style="
--sando-button-backgroundColor: purple;
--sando-button-textColor: white;
">
Custom Purple
</sando-button>
Three levels of customization:
- Flavor attribute - Broad theme change
- CSS variables - Surgical overrides
- Inline styles - One-off customizations
Migration from Other Systems β
From data-theme
β
<!-- Before: data-theme -->
<div data-theme="dark">
<button>Dark button</button>
<div data-theme="light">
<button>Light button (extra wrapper)</button>
</div>
</div>
<!-- After: flavor -->
<div flavor="dark">
<sando-button>Dark button</sando-button>
<sando-button flavor="light">Light button (no wrapper)</sando-button>
</div>
From ThemeProvider
β
// Before: ThemeProvider (React-only)
<ThemeProvider theme="dark">
<Button>Dark button</Button>
</ThemeProvider>
// After: flavor (framework-agnostic)
<div flavor="dark">
<sando-button>Dark button</sando-button>
</div>
Best Practices β
DO β β
Use flavor for broad theme changes
html<sando-button flavor="dark">Dark theme</sando-button>
Leverage inheritance for sections
html<section flavor="ocean"> <!-- All buttons here are ocean --> <sando-button>Ocean button</sando-button> </section>
Override individuals when needed
html<div flavor="dark"> <sando-button>Dark</sando-button> <sando-button flavor="vibrant">Call to Action!</sando-button> </div>
Use CSS variables for fine-tuning
html<sando-button flavor="original" style="--sando-button-backgroundColor: #custom;"> Custom </sando-button>
DON'T β β
Don't create wrapper divs for theming
html<!-- β Bad --> <div flavor="dark"> <sando-button>Button</sando-button> </div> <!-- β Good --> <sando-button flavor="dark">Button</sando-button>
Don't override every component
html<!-- β Bad (repetitive) --> <sando-button flavor="dark">Button 1</sando-button> <sando-button flavor="dark">Button 2</sando-button> <sando-button flavor="dark">Button 3</sando-button> <!-- β Good (use inheritance) --> <div flavor="dark"> <sando-button>Button 1</sando-button> <sando-button>Button 2</sando-button> <sando-button>Button 3</sando-button> </div>
Don't mix theming paradigms
html<!-- β Confusing --> <div data-theme="dark" flavor="light"> <sando-button class="theme-ocean">What am I?</sando-button> </div> <!-- β Consistent --> <div flavor="dark"> <sando-button flavor="ocean">Clear intent</sando-button> </div>
Future Enhancements β
Planned Features β
Auto-generated Flavor Palettes
css/* Generate entire flavor from single color */ :host([flavor="custom"]) { --flavor-base: #3b82f6; --flavor-light: color-mix(in oklch, var(--flavor-base) 80%, white); --flavor-dark: color-mix(in oklch, var(--flavor-base) 80%, black); }
Flavor Presets
typescript// Import pre-made flavor palettes import { oceanFlavor, sunsetFlavor } from '@sando/flavors';
Dynamic Flavor Creation
typescript// Runtime flavor generation createFlavor('brand', { primary: '#ff6b6b', contrast: 'auto', // Automatically ensure WCAG compliance });
Conclusion β
The Sando flavor system is more than just themingβit's a philosophy:
"Flexibility without complexity, consistency without rigidity"
By treating themes as "flavors" rather than rigid contexts, Sando empowers developers to:
- Mix themes freely
- Override individuals without wrappers
- Stay framework-agnostic
- Express intent through analogy
Sando isn't trying to be like other design systems. It's trying to be better.
Learn More β
- Flavor Architecture - Deep dive into flavor tokens
- Theming Guide - Practical theming tutorial
- Three-Layer System - Ingredients β Flavors β Recipes