Skip to content
Back to Blog Frontend

Tailwind vs CSS-in-JS When Your Design System Generates Code

Andrei Manolache 7 min read
Two competing geometric structures representing CSS framework approaches

When you write component styles by hand, the choice between Tailwind and CSS-in-JS is mostly about developer preference and team conventions. When those styles are generated by a pipeline, the choice has consequences that compound across every component in the library. The generator has to make the same stylistic decision hundreds of times. Getting it wrong means touching hundreds of components to fix it.

We have shipped DesignVerse output in both Tailwind and CSS-in-JS (specifically styled-components and Stitches) and have opinions about where each choice actually lands in practice. This is not an argument that one approach is universally better. Both have legitimate use cases. The argument is that the selection criteria are different when code is generated than when it is hand-authored.

What generation does to each approach

Tailwind and generation

Tailwind utility classes are strings. A generator that outputs Tailwind is generating a string of class names for each element in a component. For a Button component, the output looks like: className="bg-primary-500 text-white px-4 py-2 rounded-md text-sm font-medium hover:bg-primary-600 focus:outline-none focus:ring-2 focus:ring-primary-500".

The generator needs to know how to map a design token to a Tailwind utility class. --color-brand-primary maps to bg-primary-500 only if the Tailwind config has a primary.500 color defined. This creates a dependency: the generator and the Tailwind config must share the same token-to-utility mapping. Keeping that mapping synchronized is a non-trivial problem.

The second issue is variant handling. A Button has default, hover, focus, and disabled states. In Tailwind, each state is a separate utility class. The generator needs to enumerate every state for every property and produce the correct Tailwind modifier prefix (hover:, focus:, disabled:). This is generatable but produces verbose output that is hard to read and harder to modify by hand when you need to deviate.

The third issue is dynamic values. If a token value does not correspond to a Tailwind utility (because the token uses a custom value outside Tailwind's scale), the generator has to use Tailwind's arbitrary value syntax: bg-[#3DFFA3]. This works but defeats the point of using Tailwind's semantic utilities and produces brittle output that breaks if the token value changes.

CSS-in-JS and generation

CSS-in-JS takes JavaScript objects as style definitions. A generated styled-component or Stitches component has an object where keys are CSS properties and values are CSS custom property references or static values. The output for a Button variant might be:

const Button = styled.button({
  backgroundColor: 'var(--color-interactive-primary)',
  color: 'var(--color-text-on-primary)',
  padding: 'var(--space-2) var(--space-4)',
  borderRadius: 'var(--radius-md)',
  '&:hover': {
    backgroundColor: 'var(--color-interactive-primary-hover)',
  },
});

The mapping from token to CSS property is direct and explicit. The generator does not need to know Tailwind's class naming conventions. It needs to know CSS property names and CSS custom property names, both of which come from the same source as the token definitions.

The trade-off: CSS-in-JS requires a runtime or a build step to extract styles. styled-components adds runtime overhead. Stitches and vanilla-extract produce static CSS at build time but require more complex tooling setup. The generated output is also more dependent on the specific CSS-in-JS library, which means switching libraries later requires regenerating or rewriting every component.

The generated token coverage problem

Both approaches have a generated token coverage problem: when a component needs a value that is not in the token set, the generator either produces an arbitrary value (bad for Tailwind) or a hardcoded string (bad for CSS-in-JS). Both outcomes mean some part of the component is not driven by the token system, which undermines the single-source-of-truth goal.

The resolution is the same for both: audit the token set against the component requirements before starting generation. Every CSS value that a component needs should have a corresponding token. Values that do not exist in the token set need to be added before the generator runs. This is not a Tailwind problem or a CSS-in-JS problem; it is a completeness problem that applies to any generation approach.

What the output looks like for engineers who modify it

Generated code gets modified by engineers. A designer needs a slight one-off variant, a feature requires a specific state behavior, or a bug fix requires changing a style. The engineer opens the generated file and needs to understand it well enough to make a targeted change.

Tailwind output is immediately readable to engineers who use Tailwind daily. The class names are familiar. The pattern is predictable. An engineer can scan a className prop and understand what the component looks like without running the code.

CSS-in-JS output using CSS custom properties is readable to a different audience: engineers who are familiar with CSS property names but may not know Tailwind conventions. The variable references are explicit and self-documenting. backgroundColor: 'var(--color-interactive-primary)' tells you both what property is being styled and where the value comes from.

For teams that write components by hand in Tailwind, generated CSS-in-JS output creates a convention mismatch. Engineers are now maintaining two styling approaches in the same codebase. For teams that are comfortable with both, the mismatch is a minor friction. For teams with strong Tailwind preferences, it is a real source of resistance.

Our current recommendation for generation targets

When a team is starting fresh with a generated component library, CSS modules or CSS custom properties with plain CSS are the cleanest generation targets. They produce output that reads as standard CSS, they do not require a runtime, they work with any JavaScript framework, and they express token references directly as var() calls without a mapping layer in between.

Tailwind is the right target when: the team has an existing strong Tailwind convention, the Tailwind config is token-synchronized and stays that way, and the component library will not need arbitrary token values outside Tailwind's scale. These conditions are achievable but require deliberate setup.

Styled-components or Emotion are the right target when: the team has an existing CSS-in-JS convention, and the runtime overhead is acceptable. They are not ideal for libraries that will be consumed by multiple applications with different bundling constraints.

DesignVerse supports all three output targets. The choice of target is project-level configuration, not a capability limitation. What we have seen across usage patterns: teams that pre-select a target and configure it correctly from the start have much better outcomes than teams that choose a target and adjust it later. The generator is flexible but the downstream consumers of the generated output are not.

Stop losing hours in handoff.

Connect your design system today and generate your first components in minutes.

Free plan available. No credit card required.