Tasty logoTasty
Get Started
Tasty logoCSS-in-JS for React design systems · v3.8.0

Build components whose styles don’t fight.

Describe hover, disabled, variants, themes, and responsive behavior as state maps. Tasty makes one branch win by design—without specificity fights or source-order surprises.

Type-safeReact Server ComponentsRuntime or build timeProduction proven
Try State Maps
See How It Works
Interactive state resolver

When states overlap, only one should win.

Declare the priority in a state map.
Tasty turns that decision into selectors that cannot compete.

1. State map

JSON

The fill style, exactly as declared.

{  "fill": {    "": "#blue-accent-surface",    ":hover": "#violet-accent-surface",    ":active": "#coral-accent-surface",    "[disabled]": "#amber-accent-surface"  }}

2. Generated

CSS

Mutually exclusive selectors.

.t0.t0:not(:hover):not(:active):not([disabled])
.t0.t0:hover:not(:active):not([disabled])
.t0.t0:active:not([disabled])
.t0.t0[disabled]

3. Switch state

Test

Choose the state to simulate.

4. Preview

Live

The winning branch, applied.

Pick a state. Its source line, generated selector, and preview stay in sync.

Why Tasty

Built for long-lived component libraries where states, variants, themes, and overrides keep growing

States that don’t fight

Each property’s state map compiles into mutually exclusive selectors, so one branch wins by construction—not by source order or specificity.

Safe to extend

Add variants, overrides, and new states without re-deriving selector logic. Components stay predictable as requirements grow.

Your design system’s language

Define typed style props, tokens, units, aliases, and parser rules for your system, then compile them to standard CSS.

Built for component systems

Model roots and sub-elements together, expose governed public APIs, and compose styles as data instead of scattered overrides.

One model for every state

Pseudo-classes, variants, root and parent states, media and container queries, `:has()`, and `@supports` all use state maps.

Runtime, server, or build time

Keep the same styling model in runtime React, React Server Components and SSR, or zero-runtime build-time extraction.

Under the Hood

See how a declared priority becomes CSS selectors that cannot overlap.

Tasty DSL

Input
const Button = tasty({
  as: 'button',
  styles: {
    fill: {
      '': '#primary',
      ':hover': '#primary-hover',
      ':active': '#primary-pressed',
      '[disabled]': '#surface',
    },
  },
});

Exclusive CSS Selectors

Output
/* [disabled] wins outright */
.t0.t0[disabled] {
  background: var(--surface-color);
}

/* :active is excluded when disabled */
.t0.t0:active:not([disabled]) {
  background: var(--primary-pressed-color);
}

/* :hover is excluded when :active or disabled */
.t0.t0:hover:not(:active):not([disabled]) {
  background: var(--primary-hover-color);
}

/* default is excluded when anything above matches */
.t0.t0:not(:hover):not(:active):not([disabled]) {
  background: var(--primary-color);
}

Tasty DSL

Input
// Define a reusable state alias
configure({
  states: {
    '@dark': '@root(schema=dark) | (!@root(schema) & @media(prefers-color-scheme: dark))',
  },
});

// Use the alias in styles
const Text = tasty({
  // You can also define `@dark` here
  styles: {
    color: {
      '': '#text',
      '@dark': '#text-on-dark',
    },
  },
});

Exclusive CSS Selectors

Output
/* Branch 1: Explicit dark schema */
:root[data-schema="dark"] .t0.t0 {
  color: var(--text-on-dark-color);
}

/* Branch 2: No schema attribute + OS prefers dark */
@media (prefers-color-scheme: dark) {
  :root:not([data-schema]) .t0.t0 {
    color: var(--text-on-dark-color);
  }
}

/* Default: no schema + OS does not prefer dark */
@media (not (prefers-color-scheme: dark)) {
  :root:not([data-schema="dark"]) .t0.t0 {
    color: var(--text-color);
  }
}

/* Default: schema is set but not dark */
:root:not([data-schema="dark"])[data-schema] .t0.t0 {
  color: var(--text-color);
}

Each branch is guarded so one rule wins by construction. No specificity arithmetic. No source-order accidents.

That is what lets components compose, extend, and stay predictable as states intersect.

Try in Playground

Tokens, Units, and Color Systems

Define a shared styling language with global tokens, state-aware values, and OKHSL-friendly color authoring

Shared Tokens via configure()
configure({
  tokens: {
    '$gap': '8px',
    '$radius': '10px',
    '$border-width': '1px',
    '#surface': {
      '': '#fff',
      '@dark': 'okhsl(255 18% 12%)',
    },
    '#text': {
      '': 'okhsl(255 12% 16%)',
      '@dark': 'okhsl(255 15% 96%)',
    },
    '#primary': {
      '': 'okhsl(272 75% 55%)',
      '@dark': 'okhsl(272 70% 72%)',
    },
  },
});
Glaze Palette Generation
const violet = glaze(272, 75);

violet.colors({
  surface: {
    tone: 98, saturation: 0.2,
  },
  text: {
    base: 'surface', tone: '-62',
    contrast: 'AAA', saturation: 0.08,
  },
  'accent-surface': {
    tone: 52, mode: 'fixed',
  },
  'shadow-md': {
    type: 'shadow', bg: 'surface',
    fg: 'text', intensity: 12,
  },
});

Use configure() to define the tokens your design system owns. Those values become shared CSS custom properties, and they can use state maps too, so themes and breakpoints reuse the same vocabulary everywhere.

Tasty also supports OKHSL natively. When you want full light, dark, and high-contrast palettes with automatic WCAG-aware contrast solving, use Glaze as the companion palette generator.

See It In Action

Patterns from the recommended design-system model

State Maps

Declare intersecting states once and let Tasty generate the exclusive selectors that keep the outcome deterministic.

const Button = tasty({
  as: 'button',
  styles: {
    fill: {
      '': '#primary',
      ':hover': '#hover',
      ':active': '#pressed',
      '[disabled]': '#surface',
    },
    color: {
      '': '#on-primary',
      '[disabled]': '#text.40',
    },
    transition: 'theme',
  },
});

styleProps & modProps

Expose CSS layout controls as typed props with styleProps, and modifier states as direct props with modProps — no mods object needed.

import { tasty, POSITION_STYLES } from '@tenphi/tasty';

const Button = tasty({
  as: 'button',
  styleProps: POSITION_STYLES,
  modProps: {
    isLoading: Boolean,
    size: ['small', 'medium', 'large'] as const,
  },
  styles: {
    padding: {
      '': '1.5x 3x',
      'size=small': '1x 2x',
      'size=large': '2x 4x',
    },
    fill: {
      '': '#primary',
      isLoading: '#primary.5',
    },
    color: '#on-primary',
    radius: true,
    cursor: { '': 'pointer', isLoading: 'wait' },
  },
});

<Button size="large" placeSelf="end">Submit</Button>
<Button isLoading>Saving...</Button>

Root + Sub-Elements

Model compound components around a root state context so inner parts react together without duplicated modifier wiring.

const Alert = tasty({
  styles: {
    padding: '3x',
    fill: {
      '': '#surface',
      'type=danger': '#danger.10',
    },
    border: {
      '': '1bw solid #border',
      'type=danger': '1bw solid #danger',
    },
    Icon: {
      color: {
        '': '#text-secondary',
        'type=danger': '#danger',
      },
    },
    Message: {
      color: '#text',
    },
  },
  elements: { Icon: 'span', Message: 'div' },
});

<Alert mods={{ type: 'danger' }}>
  <Alert.Icon>!</Alert.Icon>
  <Alert.Message>Something went wrong</Alert.Message>
</Alert>

Configuration

Define the styling language once, then build components and product APIs on top of it.

import { configure } from '@tenphi/tasty';

configure({
  tokens: {
    '#primary': 'oklch(55% 0.25 265)',
    '#surface': '#fff',
    '#text': '#111',
  },
  states: {
    '@mobile': '@media(w < 768px)',
    '@dark': '@root(schema=dark)',
  },
  recipes: {
    card: {
      padding: '4x',
      fill: '#surface',
      radius: '1r',
      border: true,
    },
  },
});

Companion Tooling

Linting, editor support, and palette generation around the core engine

Glaze

Generate OKHSL-based light, dark, and high-contrast palettes with WCAG-aware contrast solving, then export them as Tasty-ready color tokens.

Colors

ESLint Plugin

Catch invalid style properties, malformed state keys, missing tokens, and other DSL mistakes before they reach the browser.

Linting

VS Code Extension

Add syntax highlighting for tokens, units, states, presets, and the rest of the Tasty authoring model inside TS and TSX files.

DX

In the Wild

Articles about the project and products powered by Tasty

Articles

Runtime CSS-in-JS deserves a rehearing

Runtime CSS-in-JS has real costs, but runtime-capable systems can solve real composition problems. Their cost should be judged against what they enable.

Read article →

Why I spent years trying to make CSS states predictable

The origin story behind Tasty: how overlapping selectors in component systems led to a compiler that makes state resolution deterministic.

Read article →

Where It's Used

Cube UI Kit

100+ production components built with Tasty from the ground up. The design system that shaped the styling model under real-world pressure.

Component Library

Cube Cloud

The universal semantic layer for data modeling, access control, caching, and APIs — powered by Cube UI Kit and Tasty in production.

Enterprise Product

tasty.style

This very website — the marketing site, docs, and interactive playground for Tasty, built with Next.js and styled entirely with Tasty.

Documentation Site

tenphi.me

Portfolio and blog of Tasty's author, built with Astro and styled with Tasty for layout, components, and article pages.

Personal Site

Make your next component predictable

Start with one component. Add states, variants, tokens, and shared conventions as your design system grows.

$ pnpm add @tenphi/tasty
Get Started
Browse Docs