Tasty logoTasty
Get Started
Tasty logotasty v1.0.0

Deterministic styling for stateful component systems

Tasty compiles state maps into mutually exclusive selectors, so component styles resolve from declared logic instead of cascade or source-order accidents.

Why Tasty

Built for reusable, stateful components that need predictable styling

Deterministic State Resolution

State maps compile into mutually exclusive selectors, so one branch wins by construction instead of through source order or specificity.

A Governed Styling Model

Design-system teams define the contracts product teams consume: shared tokens, approved patterns, override boundaries, and a consistent way to style reusable components.

Extensible Style Semantics

Define custom props, tokens, units, aliases, and parser rules for your design system, then compile them down to standard CSS output.

Recommended Methodology

The docs define a clear component model for design systems: root + sub-elements, governed public APIs, typed style props where they help, and wrapper-based extension.

Broad State Coverage

Pseudo-classes, attributes, media queries, container queries, root states, parent states, `:has()`, and `@supports` all fit into the same state-map model.

Flexible Rendering Paths

Use the same styling model in runtime React, add SSR when the app renders on the server, or choose build-time extraction when zero-runtime delivery is the goal.

How It Actually Works

Every state map compiles into mutually exclusive selectors per property

Tasty DSL

Input
const Button = tasty({
  as: 'button',
  styles: {
    fill: {
      '': '#primary',
      ':hover': '#hover',
      '[disabled]': '#surface',
    },
    color: {
      '': '#on-primary',
      '[disabled]': '#text.40',
    },
    cursor: {
      '': 'pointer',
      '[disabled]': 'not-allowed',
    },
    padding: '1.5x 3x',
    radius: 'round',
    border: 'none',
    transition: 'theme',
  },
});

Exclusive CSS Selectors

Output
/* Default: not hovered and not disabled */
.t0.t0:not(:hover):not([disabled]) {
  background: var(--primary-color);
}

/* Hovered but not disabled */
.t0.t0:hover:not([disabled]) {
  background: var(--primary-hover-color);
}

/* Disabled wins by construction */
.t0.t0[disabled] {
  background: var(--surface-color);
}

.t0.t0:not([disabled]) {
  color: var(--on-primary-color);
  cursor: pointer;
}

.t0.t0[disabled] {
  color: var(--text-color-40);
  cursor: not-allowed;
}

/* Base styles (always applied) */
.t0.t0 {
  padding: 12px 24px;
  border-radius: 9999px;
  border: none;
  transition: all var(--transition-duration)
    var(--transition-timing-function);
}

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: {
    lightness: 98, saturation: 0.2,
  },
  text: {
    base: 'surface', lightness: '-62',
    contrast: 'AAA', saturation: 0.08,
  },
  'accent-surface': {
    lightness: 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

Start with runtime, add structure as needed

Install the runtime, build a first component, then layer in shared configuration, methodology, SSR, or zero-runtime only where your system needs them.

$ pnpm add @tenphi/tasty