Skip to Content

Overrides

Sometimes one instance needs a style that doesn't belong on any axis — a button that stretches full width in a form, a card with a one-off margin. ev() has no class or css escape hatch in its config, so overrides happen at the call site.

Usage

Append styles to the css prop

The variant function returns SerializedStyles[], and Emotion's css prop accepts arrays. Put your extra styles after the variant result and they win.

<button
  css={[
    button({
      size: 'large',
    }),
    {
      width: '100%',
    },
  ]}
>
  Submit
</button>

Emotion flattens nested arrays, so spreading is optional. Both forms behave identically:

<button css={[...button({ size: 'large' }), { width: '100%' }]}>Submit</button>;

Order is the mechanism — Emotion concatenates in array order, and among declarations of equal specificity the last one wins. Put the override first and it loses:

// ✗ `width` is overwritten by base/variant styles
<button css={[{ width: '100%' }, button({ size: 'large' })]}>Submit</button>;

// ✓ the override is last
<button css={[button({ size: 'large' }), { width: '100%' }]}>Submit</button>;

Accept overrides as a prop

Give the component a prop for extra styles and append it in the same position. Callers then override without knowing how the styles are built.

import type { Interpolation, Theme } from '@emotion/react';
import type { ComponentPropsWithoutRef } from 'react';
import type { VariantProps } from 'emotion-variants';

type ButtonProps = ComponentPropsWithoutRef<'button'> &
  VariantProps<typeof button> & {
    styleOverride?: Interpolation<Theme>;
  };

function Button({ size, color, styleOverride, ...rest }: ButtonProps) {
  return <button css={[button({ size, color }), styleOverride]} {...rest} />;
}
<Button size="large" styleOverride={{ width: '100%' }}>
  Submit
</Button>

Do not name that prop css. Emotion's JSX runtime consumes css at the call site and hands the component a className instead, so a css prop never reaches Button and the override is silently dropped. Any other name is forwarded untouched.

Overriding on className surfaces

className() returns a single class, so appending another class does not reliably win — both classes have the same specificity and the winner is whichever Emotion inserted later.

// ✗ ordering is not guaranteed
<button className={`${button.className({ size: 'large' })} ${myClass}`}>Submit</button>;

Compose the styles instead of the classes, then turn the result into one class with @emotion/css:

import { css as cssClassName } from '@emotion/css';

const full = cssClassName(button({ size: 'large' }), { width: '100%' });

<button className={full}>Submit</button>;

Order only decides ties. A flat declaration never beats a more specific selector, so { width: '100%' } appended after a variant does not win while that variant's &:hover rule applies. Match the selector you are overriding:

<button
  css={[
    button({ size: 'large' }),
    {
      width: '100%',
      '&:hover': {
        width: '100%',
      },
    },
  ]}
>
  Submit
</button>;

When to add a variant instead

Reach for an override when the style belongs to one call site. If you write the same override twice, it is an axis:

// Two call sites both needed full width — make it a variant.
const button = ev({
  variants: {
    size: {
      small: {},
      large: {},
    },
    fullWidth: {
      true: {
        width: '100%',
      },
      false: {},
    },
  },
  defaultVariants: {
    fullWidth: false,
  },
});

That keeps the override path for genuine one-offs and gives the repeated case a name, a type, and a class name.

See also

Edit on GitHub

Last updated on

On this page