Tasty logoTasty
Get Started
Tasty logoCSS-in-JS for React design systems · v3.14.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.

Typed and lintedReact Server ComponentsRuntime, SSR, or build timeModern CSS built in
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",    ":hover": "#violet",    ":active": "#coral",    "[disabled]": "#amber"  }}

2. Generated

CSS

Mutually exclusive, zero-specificity selectors.

.t0.t0:where(:not([disabled]):not(:active):not(:hover))
.t0.t0:where(:not([disabled]):hover:not(:active))
.t0.t0:where(:not([disabled]):active)
.t0.t0:where([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 can’t conflict

Every state map compiles into mutually exclusive selectors, and the last matching key wins by construction. Adding a state, variant, or override never means re-deriving selector logic.

Your design system’s language

Define tokens, units, state aliases like @mobile, typography presets, and recipes once. Components then read like the spec: padding: '2x', fill: '#surface'.

Typed component APIs

Decide exactly what consumers can change. styleProps, modProps, tokenProps, variants, and sub-elements become typed props and components with autocomplete.

Guardrails in the editor and CI

An ESLint and oxlint plugin with 40+ rules checks properties, values, tokens, state keys, and presets against your config. TypeScript autocompletes your own token and preset names.

Modern CSS, one model

Media and container queries, @supports, :has(), root and parent states, and @starting-style are all state keys. Keyframes, @property, and font faces are built in.

Render anywhere

The same styles work in client React, Server Components, and streaming SSR with Next.js or Astro. Render only on the server to ship no styling runtime, or extract CSS at build time.

52 kB

for tasty() in the browser, minified and brotli-compressed

0 kB

of styling runtime when components render only on the server or CSS is extracted at build time

Only what renders

CSS is generated on demand, and equal styles share one class

Under the Hood

The same guarantee holds for harder cases: a theme alias that spans an attribute and a media query, and an extension that restyles its parent.

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
/* Explicit dark schema */
:where(:root[data-schema="dark"]) .t0.t0 {
  color: var(--text-on-dark-color);
}

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

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

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

Tasty DSL

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

// Same disabled branch as Button
const DangerButton = tasty(Button, {
  styles: {
    fill: {
      '': '#danger',
      ':hover': '#danger-hover',
      '[disabled]': '@inherit',
    },
  },
});

Exclusive CSS Selectors

Output
/* New colors, still exclusive */
.t1.t1:where(:not([disabled]):not(:hover)) {
  background-color: var(--danger-color);
}

.t1.t1:where(:not([disabled]):hover) {
  background-color: var(--danger-hover-color);
}

/* Inherited from Button */
.t1.t1:where([disabled]) {
  background-color: var(--surface-color);
}

Each branch is guarded so one rule wins by construction, and every state sits inside :where(), so the doubled class is the only specificity. 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 units, state-aware tokens, and OKHSL or OKHST colors

Units and Tokens via configure()
configure({
  units: {
    x: '8px', // padding: '2x' → 16px
    r: '10px', // radius: '1r' → 10px
  },
  tokens: {
    '#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 units and tokens your design system owns. Units compile to plain values, so 2x is 16px here. Tokens become shared CSS custom properties and can use state maps too, so themes and breakpoints reuse the same vocabulary everywhere.

Tasty reads okhsl() and okhst() colors natively. For full light, dark, and high-contrast palettes with WCAG and APCA contrast solving, use Glaze, the companion palette generator built on OKHST.

Guardrails and Tooling

Describe your design system once. The linter checks every style against it, and TypeScript autocompletes your names.

Project config
// tasty.config.ts — read by the linter and editor
export default {
  tokens: ['#surface', '#text', '#primary'],
  states: ['@mobile', '@dark'],
  presets: ['h1', 't2'],
  recipes: ['card', 'elevated'],
};

// Keep it in step with configure(). An omitted
// list is not checked.
TypeScript augmentation
// tasty.d.ts — autocomplete for your own names
import '@tenphi/tasty';

declare module '@tenphi/tasty' {
  interface TastyNamedColors {
    surface: true;
    text: true;
    primary: true;
  }
  interface TastyPresetNames {
    h1: true;
    t2: true;
  }
}
What the ESLint plugin reports
const Card = tasty({
  styles: {
    fill: '#surfce',
    // ✖ Unknown color token '#surfce'.

    padding: { '': '4x', '@mobil': '2x' },
    // ✖ Unknown state alias '@mobil'. Configured aliases: @mobile, @dark.

    color: { hovered: '#primary', '': '#text' },
    // ⚠ Default state ('') in 'color' must be the first key in the state map.

    border: '1px solid #ddd',
    // ⚠ Consider using '1bw' instead of '1px'.
    // ⚠ Use a color token instead of raw hex color '#ddd'.
  },
});

The plugin runs in the editor and in CI, with ESLint 9+ or oxlint. See the setup guide for the recommended and strict presets.

ESLint Plugin

40+ rules for ESLint and oxlint that read your tasty.config.ts and catch unknown tokens, malformed state keys, invalid values, and other DSL mistakes before they reach the browser.

Linting

VS Code Extension

Highlight tokens, units, state keys, presets, and style properties inside TS and TSX files, with snippets for components, state maps, and configure().

DX

Glaze

Generate light, dark, and high-contrast palettes from one hue in the OKHST color space, with WCAG and APCA contrast solving. Export Tasty tokens directly, or CSS, DTCG, and Tailwind.

Colors

AI Agent Brief

A one-page set of style rules to add to an agent’s context, so generated components use your tokens, units, and state maps the way a reviewer expects.

AI

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({
  modProps: {
    status: ['info', 'danger'] as const,
  },
  styles: {
    padding: '3x',
    fill: {
      '': '#surface',
      'status=danger': '#danger.10',
    },
    border: {
      '': '1bw solid #border',
      'status=danger': '1bw solid #danger',
    },
    Icon: {
      color: {
        '': '#text-secondary',
        'status=danger': '#danger',
      },
    },
    Message: {
      color: '#text',
    },
  },
  elements: { Icon: 'span', Message: 'div' },
});

<Alert status="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,
    },
  },
});

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

Cookbook

Repository-native documentation that turns project Markdown or published npm packages into polished, searchable Astro sites, with a theme powered by Tasty.

Documentation Toolkit

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