Version 0.1.0 just released! Check out the release notes from GitHub Releases

How Salty CSS Resurrects the Beloved styled API with Zero-Runtime Performance and TypeScript-First DX

The Styled API

When the frontend ecosystem realized that runtime CSS-in-JS libraries were too heavy for modern server-first architectures, the pendulum swung hard in the opposite direction. We saw a massive migration toward utility classes and raw string generators.

But the community conflated two different things: the runtime performance cost was the problem, not the developer experience (DX). The declarative, component-based API pioneered by libraries like styled-components, Emotion, and Stitches remains the cleanest, most maintainable way to build atomic design systems.

Salty CSS resurrects the beloved styled API without the performance penalty. By blending the best ideas from Stitches and Panda CSS with aggressive build-time extraction, it provides an opinionated, TypeScript-first component factory that feels like magic but compiles to zero-runtime CSS.

Why Components Over Class Names?

Salty CSS does export a className() function that produces plain string outputs. It is a fantastic, framework-agnostic escape hatch if you need to port core styles to an unsupported framework (like Angular) or wire up complex third-party DOM structures.

However, the primary API is the styled() component factory, and this is a deliberate, opinionated choice.

Building atomic components with raw class names requires manual boilerplate: you have to create a wrapper function, accept props, map those props to variant classes, and forward HTML attributes. The styled API eliminates this entirely. Furthermore, extending styles by wrapping components (styled(Button)) automatically manages CSS cascade layers, which is infinitely more predictable than trying to concatenate and order class name strings and hoping the specificities resolve correctly.

A component creates a strict, discoverable contract. A class name is just a string.

The TypeScript Illusion (DX Magic)

Salty CSS is TypeScript-first, but the typing goes far beyond basic property suggestions. The API is designed to create a completely seamless editing experience in your IDE:

  • Token Autocomplete: You never have to memorize your design tokens. Anywhere a value is accepted, you can type { and your IDE will suggest your exact token paths (e.g., {colors.brand.main}). These paths are validated at build time, meaning typos surface as compiler errors rather than silent visual bugs in the browser.

  • Named Media Queries: Instead of writing raw CSS media queries, you can reference the aliases defined in your config directly as object keys (e.g., '@tabletDown': { ... }).

  • Template Resolution: If you define a typography system using the defineTemplates factory, your IDE will autocomplete the valid paths (like textStyle: "heading.large") directly inside your style objects.

  • Component Instance Autocomplete: When a developer imports your atomic component, the generated JSX element intrinsically knows its available variants. The variants become strict, typed union props.

These might sound like small quality-of-life improvements, but in a large codebase, they drastically reduce cognitive load and documentation lookups.

The Props Philosophy: Build-Time Data Passing

In legacy runtime CSS-in-JS, you could pass a function to a style block and interpolate props dynamically ((props) => props.color). Because Salty CSS extracts styles at build time, this pattern is impossible—and realistically, it was always the primary culprit for severe performance degradation.

Salty CSS approaches prop-driven styling through strict architectural boundaries.

1. Variants and defaultVariants

For discrete states, you define variants, compoundVariants, and anyOfVariants. The styled factory automatically maps these to JSX props. You can establish the baseline state using defaultVariants, which determines the styling branch applied when the consumer omits a variant prop.

2. Typed Prop Tokens (css-* props)

For continuous or completely dynamic values (like a user-defined hex color), Salty CSS introduces Typed Prop Tokens. You reference a placeholder in your .css.ts file using {props.bgColor}.

This automatically exposes a typed css-bg-color prop on the rendered JSX component. Under the hood, Salty CSS intercepts this prop and injects it as an inline --props-bg-color CSS custom property. This gives you the flexibility of inline styles but maintains a strict, type-checked API contract without exposing an arbitrary style={{}} object to the consumer.

3. passProps: The Opt-In Forwarding Allowlist

A historic pain point with CSS-in-JS is prop swallowing: the library consumes a prop for styling, preventing it from reaching the underlying HTML element. Other libraries attempted to fix this using ugly prefixes (like $disabled).

Salty CSS solves this by making variant props strictly local by default. If you define a disabled variant, Salty uses it for CSS and drops it. To push it to the DOM, you use the passProps allowlist:

export const Button = styled("button", {
  passProps: ["disabled"], // Forwards the prop to the HTML <button>
  base: { ... },
  variants: {
    disabled: { true: { opacity: 0.5 } }
  }
});

The passProps configuration is essential in two scenarios:

  1. HTML Attribute Parity: When a variant name (like disabled or required) is also a necessary semantic HTML attribute.
  2. Component Wrapping: When you are extending a complex third-party component (like Next.js <Link>) that fundamentally requires specific props (like href) to function.

4. defaultProps vs defaultVariants

To maintain the boundary between styling logic and DOM structure, Salty separates defaults. While defaultVariants controls CSS branches, defaultProps allows you to bind native HTML attributes directly to the atomic component. For example, defining defaultProps: { target: "_blank", rel: "noreferrer" } on an external link component guarantees semantic correctness without cluttering your variant logic.

Zero Specificity: anyOfVariants

The variant API is heavily influenced by Stitches, but enhanced with modern CSS capabilities. A prime example is anyOfVariants.

While compoundVariants requires an exact match of multiple factors (X AND Y), anyOfVariants serves as an "OR" logic gate. It allows you to declare shared styles across multiple variant triggers (e.g., "if intent is warning OR danger, apply this bold border").

Architecturally, this is brilliant because the Salty compiler wraps anyOfVariants rules inside the native CSS :where() pseudo-class. The :where() wrapper carries zero specificity. This means you can declare broad, sweeping rules across your component, knowing with absolute certainty that a more specific, direct variants rule will effortlessly override it without any specificity conflicts.