How to Override React Component Styles with vanilla-extract
When transitioning from runtime CSS-in-JS libraries (like styled-components or Emotion) to zero-runtime solutions like vanilla-extract, developers often encounter a common specificity hurdle. Because vanilla-extract emits static CSS classes at build time, simply concatenating class names like className={[styles.base, styles.override].join(' ')} creates a source-order dependency. In CSS, if two classes have identical specificity, whichever rule appears later in the generated stylesheet wins—regardless of the order of classes in your HTML.
Here are the best, production-tested patterns to cleanly override styles in React components using vanilla-extract.
1. The Modern Standard: CSS Layers (layer)
Since vanilla-extract supports native CSS Cascade Layers (@layer), this is by far the cleanest and most scalable way to enforce style precedence. Rules declared in a higher-priority layer always beat rules in a lower-priority layer, irrespective of specificity or bundling order.
// layers.css.ts
import { layer } from '@vanilla-extract/css';
// Create layers in order of precedence: base first, overrides second
export const baseLayer = layer();
export const overrideLayer = layer();
// my-component.css.ts
import { style } from '@vanilla-extract/css';
import { baseLayer, overrideLayer } from './layers.css';
export const root = style({
@layer: {
[baseLayer]: {
background: 'green',
fontWeight: 'bold',
},
},
});
export const awesome = style({
@layer: {
[overrideLayer]: {
background: 'red',
},
},
});
Now, regardless of how your CSS bundler orders the emitted CSS rules, overrideLayer will always take precedence over baseLayer.
2. The Idiomatic Component Pattern: @vanilla-extract/recipes
If you find yourself overriding styles to represent variations of a component (e.g., primary vs. secondary buttons, sizes, or variants), you should use the official @vanilla-extract/recipes package. It is specifically designed to eliminate class-merging bugs by structuring default styles and overrides declaratively.
npm install @vanilla-extract/recipes
// my-component.css.ts
import { recipe } from '@vanilla-extract/recipes';
export const componentRecipe = recipe({
base: {
fontWeight: 'bold',
background: 'green',
},
variants: {
variant: {
awesome: {
background: 'red',
},
},
},
defaultVariants: {},
});
In your components, you can pass variant props instead of trying to override raw CSS classes:
// MyAwesomeComponent.tsx
import { componentRecipe } from './my-component.css';
export const MyAwesomeComponent = ({ variant, className, ...props }) => (
<div className={[componentRecipe({ variant }), className].join(' ')} {...props} />
);
// MyMoreAwesomeComponent.tsx
export const MyMoreAwesomeComponent = (props) => (
<MyAwesomeComponent variant="awesome" {...props} />
);
3. Dynamic Property Control via CSS Variables
Another clean zero-runtime approach is to expose dynamic or customizable styles as CSS variables. Instead of overriding an entire rule, your parent component passes a style contract down to the base component.
// my-component.css.ts
import { style, createVar } from '@vanilla-extract/css';
export const bgVar = createVar();
export const root = style({
background: bgVar,
fontWeight: 'bold',
vars: {
[bgVar]: 'green', // Default fallback
},
});
export const awesome = style({
vars: {
[bgVar]: 'red',
},
});
When applying awesome alongside root, it simply alters the value of the custom property rather than colliding with the background property declaration directly.
4. Specificity Bumping (The Pragmatic Escape Hatch)
If you cannot refactor your component hierarchy and need a quick fix, you can artificially bump the specificity of the overriding class using the && selector.
// my-component.css.ts
import { style } from '@vanilla-extract/css';
export const root = style({
background: 'green',
fontWeight: 'bold',
});
export const awesome = style({
selectors: {
'&&': {
background: 'red',
},
},
});
Note: While this reliably doubles the class selector's specificity (rendering as .awesome.awesome), rely on it sparingly as specificity battles can quickly become hard to maintain across large design systems.
Summary: Which Approach Should You Choose?
- Use CSS Layers (
@layer): Best general pattern for utility classes, extending third-party components, or building layered UI libraries. - Use Recipes (
@vanilla-extract/recipes): Best if the styles represent known variants, sizes, or visual states of your components. - Use CSS Variables: Best when styling dynamic token values (colors, spacing, dimensions) that need to cascade cleanly.