Tasty logoTasty
Playground

Adoption Guide

Tasty is most useful where reusable component styling has become a state-resolution problem: hover intersects with disabled, variants intersect with themes, and every extension makes selector behavior harder to predict.

It normally sits underneath a design system’s existing components. You can preserve their public React APIs while moving their style logic into state maps that produce one declared winner. Adoption should start with a small, state-heavy component—not an all-at-once rewrite.

This guide is for design-system maintainers and platform engineers evaluating Tasty or introducing it into an existing codebase. Use it for rollout strategy and adoption sequencing; use the Comparison guide when the open question is whether Tasty is the right tool.


Where Tasty sits in the stack

Tasty is not the surface your product engineers interact with directly. It sits one layer below:

Product code
  └─ DS components (Button, Card, Layout, ...)
       └─ Tasty engine (tasty(), configure(), style functions)
            └─ CSS (mutually exclusive selectors, tokens, custom properties)

What Tasty owns:

What the DS team owns:

Two teams using Tasty can end up with very different authoring models. That is by design.


Who should adopt Tasty

Strong fit:

Not the right fit:

For a detailed comparison with Tailwind, Panda CSS, vanilla-extract, StyleX, Stitches, and Emotion, see the Comparison guide.


What you are expected to define

Tasty provides the engine. The DS team defines the language that runs on it. Here is what that typically involves:

LayerWhat you defineWhere
TokensColor names, spacing scale, border widths, radiiconfigure({ tokens })
UnitsCustom multiplier units (x, r, bw, or your own)configure({ units })
State aliasesResponsive breakpoints, theme modes, feature flagsconfigure({ states })
RecipesReusable style bundles (card, elevated, input-reset)configure({ recipes })
TypographyPreset definitions (h1-h6, t1-t4, etc.)configure({ presets: { ... } })
Style propsWhich CSS properties each component exposes as React propsstyleProps in each component
Sub-elementsInner parts of compound components (Title, Icon, Content)elements + capitalized keys in styles
Override rulesHow product engineers extend or constrain componentsStyled wrappers via tasty(Base, { ... })

The same engine can power a minimal design system with a handful of tokens:

configure({
  tokens: { '#bg': '#white', '#text': '#111' },
  states: { '@dark': '@root(schema=dark)' },
});

...or an enterprise-scale system with dozens of tokens, multiple state aliases, typography presets, recipes, and custom units. The scope is yours to decide.

Here is how the layers connect end-to-end. The DS team configures the engine, defines components, and product engineers consume them:

// ds/config.ts — DS team defines the language
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 },
  },
});

// ds/components/Card.tsx — DS team builds components on top
const Card = tasty({
  styles: {
    recipe: 'card',
    Title: { preset: 'h3', color: '#primary' },
    Body: { preset: 't2', color: '#text' },
  },
  elements: { Title: 'h2', Body: 'div' },
  styleProps: ['padding', 'fill'],
});

// app/Dashboard.tsx — product engineer uses the component
<Card padding={{ '': '4x', '@mobile': '2x' }}>
  <Card.Title>Monthly Revenue</Card.Title>
  <Card.Body>$1.2M — up 12% from last month</Card.Body>
</Card>;

See Configuration for the full configure() API.


Incremental adoption

You do not need to adopt everything at once. Tasty is designed to be introduced layer by layer.

A practical first slice is one component that already suffers from intersecting state and variant logic: a button, input, menu item, disclosure, or interactive card. Keep its public API unchanged, move one or two properties into Tasty state maps, and verify that the declared priority matches the behavior your team expects.

This proves the differentiator before you invest in a complete token vocabulary or migrate an entire component library.

Phase 1 -- Pilot one state-heavy component

Choose a component with a known hover/disabled, focus/error, or variant/theme interaction. Preserve its external props and visual behavior. The pilot should answer three questions:

  1. Is the state map easier to review than the existing selectors or conditional class logic?
  2. Does extending the component keep the intended state priority?
  3. Can your current tokens and component API move over without product-code churn?

If the answer is yes, establish the shared design-system vocabulary around the successful slice.

Phase 2 -- Tokens and units

Start by defining your design tokens and custom units. This is the lowest-risk step: it only configures the parser and does not require rewriting any components.

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

configure({
  tokens: {
    '#primary': 'oklch(55% 0.25 265)',
    '#surface': '#white',
    '#text': '#111',
    '$card-padding': '4x',
  },
  // Common units (x, r, bw, ow, cr) are built-in.
  // A DS typically redefines them to use CSS custom properties
  // so that the actual scale is controlled via CSS, not JS:
  units: {
    x: 'var(--gap)', // 2x → calc(var(--gap) * 2)
    r: 'var(--radius)',
    bw: 'var(--border-width)',
  },
});

Phase 3 -- State aliases and recipes

Define the state vocabulary your components will share. This is where you start encoding your team's conventions.

configure({
  states: {
    '@mobile': '@media(w < 768px)',
    '@tablet': '@media(w < 1024px)',
    '@dark':
      '@root(schema=dark) | (!@root(schema) & @media(prefers-color-scheme: dark))',
  },
  recipes: {
    card: { padding: '4x', fill: '#surface', radius: '1r', border: true },
    elevated: { shadow: '0 2x 4x #shadow' },
  },
});

Phase 4 -- Migrate a few primitives

Pick 2-3 widely used primitives (Box, Text, Button) and rewrite them with tasty(). Keep the public API identical so product code does not need to change.

const Box = tasty({
  as: 'div',
  styles: {
    display: 'flex',
    flow: 'column',
    gap: '1x',
  },
  styleProps: ['gap', 'flow', 'padding', 'fill'],
});

At this point you can validate the DSL, token workflow, and component authoring experience before expanding the rollout.

Phase 5 -- Expand state resolution

Move the components with the most painful intersecting states (buttons with hover + disabled + theme variants, inputs with focus + error + readonly) to Tasty's state map syntax. This is where mutually exclusive selectors start paying off.

const Button = tasty({
  as: 'button',
  styles: {
    fill: {
      '': '#primary',
      ':hover': '#primary-hover',
      ':active': '#primary-pressed',
      // `disabled` is a data-attribute modifier → [data-disabled].
      // Tasty auto-applies it from the native `disabled` attribute.
      // `[disabled]` (attribute selector) also works here.
      disabled: '#surface',
    },
    color: {
      '': '#on-primary',
      disabled: '#text.40',
    },
    cursor: {
      '': 'pointer',
      disabled: 'not-allowed',
    },
    transition: 'theme',
  },
});

Phase 6 -- Standardize style props and sub-elements

Define which style props each component category exposes. Layout components get flow/gap/padding. Interactive components get positioning. Compound components declare sub-elements.

const Card = tasty({
  styles: {
    recipe: 'card elevated',
    Title: { preset: 'h3', color: '#primary' },
    Content: { color: '#text', preset: 't2' },
  },
  elements: { Title: 'h2', Content: 'div' },
  styleProps: ['padding', 'fill', 'radius'],
});

Phase 7 -- Expand to full DS coverage

Migrate the remaining components, add the ESLint plugin to enforce style conventions at lint time, and choose between server-only tasty() and tastyStatic() build-time extraction when a page should ship no Tasty styling runtime.


What changes for product engineers

When a DS is powered by Tasty, product engineers typically interact with components, not Tasty itself. Here is what changes from their perspective:

They do not write CSS directly. Styling decisions are embedded in the components the DS provides. Product code consumes components, tokens, and style props.

Overrides use styled wrappers. Instead of passing one-off className or style props, product engineers extend components:

import { tasty } from '@tenphi/tasty';
import { Button } from 'my-ds';

// Replace mode: providing '' (default) key replaces the parent's fill entirely
const DangerButton = tasty(Button, {
  styles: {
    fill: { '': '#danger', ':hover': '#danger-hover' },
  },
});

// Extend mode: omitting '' key preserves parent states and adds/overrides
const LoadingButton = tasty(Button, {
  styles: {
    fill: {
      loading: '#yellow', // new state appended
      disabled: '#gray.20', // existing state overridden in place
    },
  },
});

Style props replace raw CSS. Layout, spacing, and positioning are controlled through typed props on the components that expose them:

<Space flow="row" gap="2x" placeItems="center">
  <Title>Dashboard</Title>
  <Button placeSelf="end">Add Item</Button>
</Space>

Components are server components by default. All tasty() components and style functions are hook-free, so they work as React Server Components without 'use client'. In server-only contexts they ship no Tasty styling runtime. Astro without client:* directives ships no client JavaScript; server-only Next.js RSC follows the same Tasty architecture, while final output depends on the application deployment. Product engineers only add 'use client' when their component needs actual React interactivity (state, effects, event handlers), never because of styling.

No cascade/specificity concerns. Tasty's mutually exclusive selectors mean extending a component cannot accidentally break another. Import order, class name collisions, and specificity arithmetic are non-issues.


Migration and Interop Notes


Learn more