@tenphi/tasty is a CSS-in-JS styling system and DSL for React. It provides declarative, state-aware styling with design token integration, sub-element styling, and zero-runtime extraction via Babel.
Repository: https://github.com/tenphi/tasty
| Command | Purpose |
|---|---|
pnpm build |
Build via tsdown (ESM, browser + node targets) |
pnpm test |
Run tests (vitest — both projects) |
pnpm test:node |
Run only the node project (pure logic, no DOM) |
pnpm test:browser |
Run only the browser project (headless Chromium) |
pnpm test:setup |
Download the Chromium binary the browser project needs |
pnpm typecheck |
Type-check without emitting |
pnpm lint |
Lint source files |
pnpm lint:fix |
Lint and auto-fix |
pnpm format |
Format with Prettier |
pnpm format:check |
Check formatting |
pnpm bench |
Run pipeline benchmarks (Node project — the numbers quoted in the README) |
pnpm bench:browser |
Run component-render benchmarks (headless Chromium) |
pnpm bench:interaction |
Run the steady-state interaction benchmark — mod flips and subtree churn (production React, headless Chromium) |
pnpm bench:cold-start |
Run the page-load cold-start benchmark — transfer, compile, execute, first paint under CDP throttling (needs pnpm build first) |
pnpm size |
Check bundle sizes (size-limit) |
pnpm check:test-only |
Verify test-only code stays out of the build (run after pnpm build) |
pnpm hygiene |
Run lint + format check + typecheck together |
pnpm hygiene:fix |
Auto-fix lint + format, then typecheck |
Follow the ordered steps in .cursor/commands/submit-changes.md for the full release-oriented workflow. Before you push any branch, at minimum:
- Typecheck — Run
pnpm typecheck. If it fails, stop and fix errors before formatting or committing. - Lint — Run
pnpm lint. If it fails, stop and fix errors before formatting or committing. - Format — Run
pnpm formatso committed code matches Prettier output. - Public API — If
src/__snapshots__/public-api.mdchanged, the diff is a public API change (an added, removed, or renamed export on apackage.jsonsubpath). Review it line by line and make sure a changeset covers it. Never update that snapshot without one. - Changeset — If the change affects published package behavior (features, fixes, refactors, perf), create a changeset file in
.changeset/as described insubmit-changes.mdand include it in the commit. Usepatchfor fixes/small changes,minorfor new features/non-breaking API changes,majorfor breaking changes. Skip the changeset only when the change is purely internal (docs, CI, repo-only churn, tests with no behavior change). - Commit — Use Conventional Commits (
feat,fix,refactor,test,docs,chore,perf,ci; optional scope). Keep the subject line short. Include the changeset file in the same commit. - Push — Do not push to
main. Confirm the current branch, then push withgit push -u origin HEAD.
- Language: TypeScript (strict mode,
consistent-type-importsenforced) - Build: tsdown — ESM, unbundled, dts + sourcemaps, browser + node targets
- Test: Vitest 4, globals enabled, split into two projects (see Test environments)
- Lint: ESLint 10 + typescript-eslint + prettier +
@tenphi/eslint-plugin-tasty(dogfooded on the repo's own style objects; seeeslint.config.jsfor the rules disabled because this repo tests the parser) - Format: Prettier — single quotes, semicolons, trailing commas, 80 cols
- Versioning: Changesets
- Runtime: Node >= 20, pnpm 11
| Import path | Description | Platform |
|---|---|---|
@tenphi/tasty |
Runtime style engine (tasty, hooks, configure) | Browser |
@tenphi/tasty/core |
Core engine without SSR | Browser |
@tenphi/tasty/static |
Build-time static style generation (tastyStatic) | Browser |
@tenphi/tasty/static/inject |
Runtime helper the Babel plugin rewrites tastyStatic imports to in inject mode |
Browser |
@tenphi/tasty/babel-plugin |
Babel plugin for zero-runtime CSS extraction | Node |
@tenphi/tasty/zero |
Programmatic zero-runtime extraction API | Node |
@tenphi/tasty/zero/next |
Next.js integration wrapper for zero-runtime | Node |
@tenphi/tasty/ssr |
Server-side rendering collector + hydration | Node |
@tenphi/tasty/ssr/next |
Next.js App Router SSR integration | Node |
@tenphi/tasty/ssr/astro |
Astro integration + middleware | Node |
@tenphi/tasty/ssr/astro-client |
Astro client-side cache hydration | Browser |
@tenphi/tasty/ssr/astro-middleware@tenphi/tasty/ssr/astro-middleware-static |
Middleware entrypoints tastyIntegration() hands to Astro's addMiddleware(). Exported only so Astro can resolve them by specifier — never reference them directly. |
Node |
src/
index.ts Main entry point (runtime exports)
tasty.tsx Core tasty() factory — creates styled React components
config.ts Global configuration system (configure())
types.ts Core TypeScript types
debug.ts Runtime debug/diagnostic utilities (tastyDebug)
core/ Core engine without SSR side-effects
static/ tastyStatic() — build-time style generation
zero/ Zero-runtime CSS extraction & Babel plugin
babel.ts Babel plugin entry
next.ts Next.js wrapper
extractor.ts Style extraction logic
css-writer.ts CSS file writer
hooks/ React hooks
useStyles.ts Generate className from style definitions
useGlobalStyles.ts Inject global styles for a selector
useRawCSS.ts Inject raw CSS strings
useKeyframes.ts Inject @keyframes animations
useProperty.ts Inject CSS @property definitions
useFontFace.ts Inject @font-face definitions
useCounterStyle.ts Inject @counter-style definitions
useFunction.ts Inject CSS @function (custom function) definitions
injector/ Runtime CSS injection engine
injector.ts Core injector (hash dedup, ref counting, cleanup)
sheet-manager.ts CSSStyleSheet management
pipeline/ Style rendering pipeline (parse → exclusives → materialize); see docs/pipeline.md
parser/ Style value parser & tokenizer (custom DSL)
styles/ Style property handlers (fill, padding, border, etc.)
chunks/ Style chunking system
states/ Predefined state mappings (@hover, @media, etc.)
plugins/ Plugin system (OKHSL color support, etc.)
keyframes/ @keyframes support
properties/ CSS @property support
functions/ CSS @function support (+ opt-in polyfill)
font-face/ @font-face support
counter-style/ @counter-style support
prop-handlers.ts Props middleware registry (configure({ propHandlers }))
ssr/ Server-side rendering (collector, hydration, framework bindings)
utils/ Shared utilities
tasty(options)— create a styled React componenttasty(BaseComponent, options)— extend an existing component with stylesconfigure(opts)— set global config (tokens, replaceTokens, units, states, functions, polyfills, keyframes, properties, fontFaces, counterStyles, recipes, presets, handlers, propHandlers, baseStyleProps, globalStyles, plugins)useStyles(styles)— generate a className from a style objectuseGlobalStyles(selector, styles)— inject global stylesuseRawCSS(css)— inject raw CSSuseKeyframes/useProperty/useFontFace/useCounterStyle/useFunction— inject the corresponding at-ruletastyStatic(styles)— build-time style generation (zero runtime)
| File | Description |
|---|---|
docs/migration-v3.md |
Migration guide v2 → v3 — every breaking change with a search-and-replace cheat sheet, plus an explicit "not changed" list. Update it whenever a major changeset is added. |
docs/README.md |
Documentation hub — routes readers by role, rendering mode, and task across onboarding, API docs, internals, and debugging. |
docs/getting-started.md |
Getting started guide — prerequisites, installation, first component, configuration setup, ESLint plugin setup, editor tooling, rendering mode decision tree. Start here for initial setup. |
docs/methodology.md |
Methodology — the recommended patterns for structuring Tasty components: root + sub-elements model (vs BEM), styleProps as the public API, tokens prop, styles vs style props, wrapping/extension, how configuration simplifies components, and anti-patterns. |
docs/design-system.md |
Building a design system — practical how-to for DS teams: designing token vocabularies, defining state aliases, creating recipes, building layout primitives with styleProps, compound components with sub-elements, override contracts, and project structure. |
docs/ai-agents.md |
Style rules for AI agents — condensed, rule-based brief on correct value syntax, tokens, units, modifiers, state maps, sub-elements, and special top-level keys. Keep in sync with the DSL and style-property docs. |
docs/dsl.md |
Style DSL reference — the Tasty style language shared by runtime and static modes: state maps, state key types, color tokens, built-in units, replace tokens, recipes, extending/replacing semantics, advanced states (@media, @parent, @root, :is, :has), keyframes, @property, @font-face, @counter-style, and @function. |
docs/react-api.md |
React API — tasty() factory, component creation, extending, styleProps, modProps, tokenProps, variants, sub-element styling (elements prop, selector affix), and style functions (useStyles, useGlobalStyles, useRawCSS, useKeyframes, useProperty, useFontFace, useCounterStyle, useFunction). |
docs/configuration.md |
Global configuration via configure() — CSP nonce, custom state aliases, parser cache size, custom units, custom functions, polyfills, design tokens (:root CSS variables), replace tokens (parse-time substitution), recipes, style handlers, props middleware, base style props, and plugins. |
docs/plugins.md |
Plugins & extension points — what a plugin is, how to choose between functions/units/states/handlers/propHandlers/recipes/baseStyleProps, the style-handler and props-middleware contracts, typing your extension, and a worked end-to-end plugin. |
docs/styles.md |
Style properties reference — documents all custom style handlers (fill, padding, margin, border, radius, flow, preset, shadow, outline, display, width/height, gap, inset, fade, scrollbar) with their enhanced syntax and modifiers. |
docs/tasty-static.md |
Zero-runtime mode (tastyStatic) — build-time CSS generation for static sites and performance-critical pages. Covers Babel plugin setup, Next.js integration, static config files, and limitations. |
docs/pipeline.md |
Style rendering pipeline — stages from parsed state keys through exclusive conditions, handler snapshots, merge-by-value, and CSS materialization; condition types, simplification, and caching. Implementation in src/pipeline/. |
docs/injector.md |
Internal style injector architecture — hash-based deduplication, reference counting, CSS nesting flattening, keyframes injection, sheet management, SSR support, and Shadow DOM roots. Low-level infrastructure doc. |
docs/debug.md |
Debug utilities (tastyDebug) — runtime CSS inspection, cache performance metrics, style chunk analysis, and troubleshooting via browser console. Development-only diagnostics. |
docs/ssr.md |
Server-side rendering guide — zero-cost hydration, ServerStyleCollector, framework integrations (Next.js App Router, Astro), streaming compatibility. Requires React 18+. |
docs/comparison.md |
Comparison with other styling systems — Tailwind, Panda CSS, vanilla-extract, StyleX, Stitches, Emotion. Covers positioning, abstraction levels, trade-offs, and when Tasty fits vs. alternatives. |
docs/adoption.md |
Adoption guide — where Tasty sits in the stack, who should use it, what the DS team defines, incremental adoption phases, and what changes for product engineers. |
- TypeScript strict mode;
consistent-type-importsenforced - Test files:
*.test.ts/*.test.tsx, co-located insrc/ - Unused variables prefixed with
_are allowed - JSX transform:
react-jsx(noimport Reactneeded) - Functional API pattern: factory functions + hooks, no class components. The styling API (
tasty,useStyles,configure, etc.) is entirely functional. Stateful infrastructure services (ServerStyleCollector,CSSWriter,StyleInjector) are classes but each exposes acreate*()factory wrapper (createServerStyleCollector,createCSSWriter) as the canonical public entry point; the class is also exported for advanced/internal use. - All style values go through the Tasty parser — supports design tokens (
#color,$token), custom units (2x,1r), auto-calc, and color opacity (#purple.5)
Vitest runs two projects, configured in vitest.config.ts:
| Project | Environment | Covers |
|---|---|---|
node |
plain Node, no DOM | Parser, style handlers, pipeline, SSR string output, Babel extractor, config merging — pure logic |
browser |
headless Chromium via Playwright | Everything that touches document, renders React, or asserts on CSS the engine actually parsed |
Where a new test goes. Add it to the BROWSER_TESTS list in vitest.config.ts if it touches document, renders React, or asserts on injected CSS; otherwise it lands in node automatically. All *.test.tsx files are already matched by the list. A DOM test left in the node project fails loudly with document is not defined, so the mistake is cheap.
Why a real browser. Tasty compiles to CSS, and only a CSS engine can tell you whether that CSS is valid. jsdom and happy-dom reject @container, @starting-style, @property, @function, and CSS nesting outright — under jsdom, 53 of the 54 snapshots in advanced-states.test.tsx were empty strings asserting nothing, and @container/@starting-style coverage did not exist at all. Chromium accepts these rules, so the snapshots now pin real CSS and an invalid declaration shows up as a dropped property.
Consequences to keep in mind:
- The engine reserializes what it accepts.
oklch(var(--x)/.1)comes back asoklch(var(--x) / .1), andCSSKeyframesRule.cssTextspans multiple lines. Assert with tolerant matchers, or useforceTextInjection: truewhen the point is to pin Tasty's own output byte-for-byte (seeinjector/at-rule-docs.test.ts). - Degradation paths must be simulated, not inherited. Chromium supports
@property, so the "engine has no@propertysupport" branch is exercised by stubbingCSSStyleSheet.prototype.insertRule— seesimulateNoAtPropertySupport()ininjector/injector.test.ts. - There is no
processglobal in the browser, sovi.stubEnv('NODE_ENV', …)cannot reachisDevEnv(). UseenableDevWarnings()fromsrc/test/dev-env.ts, which flips theTASTY_DEBUGlocalStorage flag — the switch that works in a real browser. - First checkout needs
pnpm test:setuponce to download the Chromium binary.
- CI: build, lint, format check, typecheck, dead-code check (
knip), test-only code check, tests, size limit on push tomainand PRs. Chromium is installed via Playwright and cached on the lockfile hash. - Release: Changesets — on push to
main, either creates a version PR or publishes to npm - Snapshots: comment
/snapshoton a PR for0.0.0-snapshot.<sha>release - npm trusted publishing: OIDC provenance via the
releaseGitHub environment
- No runtime dependencies except
csstype(CSS type definitions) andjiti(config file loading) - Hash-based class names (
t0,t1, ...) — deterministic within a render, deduped by content hash - Reference counting for component styles (
tasty(),useStyles) — swept by GC once unreferenced. Styles from the standalone functions (useGlobalStyles,useRawCSS,useKeyframes, …) are not cleaned up on unmount; they are replaced per slot (id/selector/name, perroot) - Streaming-compatible SSR — works with
renderToPipeableStreamand framework streaming - Plugin system — extensible via
configure({ plugins: [...] })for custom color spaces, style handlers, props middleware, and more; seedocs/plugins.md