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.
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
JSONThe fill style, exactly as declared.
{ "fill": { "": "#blue", ":hover": "#violet", ":active": "#coral", "[disabled]": "#amber" }}2. Generated
CSSMutually 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
TestChoose the state to simulate.
4. Preview
LiveThe 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
Inputconst 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.
Tokens, Units, and Color Systems
Define a shared styling language with units, state-aware tokens, and OKHSL or OKHST colors
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%)',
},
},
});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.
// 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.// 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;
}
}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.
LintingVS Code Extension
Highlight tokens, units, state keys, presets, and style properties inside TS and TSX files, with snippets for components, state maps, and configure().
DXGlaze
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.
ColorsAI 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.
AISee 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 LibraryCube Cloud
The universal semantic layer for data modeling, access control, caching, and APIs — powered by Cube UI Kit and Tasty in production.
Enterprise Producttasty.style
This very website — the marketing site, docs, and interactive playground for Tasty, built with Next.js and styled entirely with Tasty.
Documentation Sitetenphi.me
Portfolio and blog of Tasty's author, built with Astro and styled with Tasty for layout, components, and article pages.
Personal SiteCookbook
Repository-native documentation that turns project Markdown or published npm packages into polished, searchable Astro sites, with a theme powered by Tasty.
Documentation ToolkitMake your next component predictable
Start with one component. Add states, variants, tokens, and shared conventions as your design system grows.
$ pnpm add @tenphi/tasty