Use `$` Transient Props in styled-components to Keep Styling Inputs Out of the DOM
React component props get overloaded fast. Prefixing styling-only values with `$` in styled-components makes them transient, so they never reach the DOM or wrapped component. Here is how the convention works, where it breaks, and how to verify it locally.
28 Jun 2026, 10:43 UTC

When props leak into the DOM
React component APIs get overloaded quickly. A Button that accepts variant and size feels natural until you render it and find those strings sitting in the DOM as invalid HTML attributes. With styled.button, styled-components filters unknown attributes for you, but the moment you wrap your own component with styled(MyComponent), every prop is forwarded to MyComponent. If that component renders a DOM node, you either write manual filter logic or accept noisy markup. The root issue is not styling; it is that props are doing two jobs at once—carrying behavior and carrying presentation inputs.
Transient props: the $ prefix convention
styled-components supports transient props. Any prop prefixed with a dollar sign is consumed by the styled component and is not forwarded to the underlying DOM element or the wrapped component. This turns a naming convention into a forwarding rule enforced by the library.
A Button example
Define a styled button where $variant and $size exist only inside the CSS template literal:
import styled from 'styled-components';
type ButtonProps = {
$variant?: 'primary' | 'secondary';
$size?: 'sm' | 'md';
};
const Button = styled.button<ButtonProps>`
padding: ${({ $size }) => ($size === 'sm' ? '4px 8px' : '8px 16px')};
background: ${({ $variant }) => ($variant === 'primary' ? '#0366d6' : '#6f42c1')};
color: white;
border: none;
border-radius: 4px;
`;
// Usage: <Button $variant="primary" $size="sm">Save</Button>
When rendered, the <button> element receives normal HTML attributes such as type, disabled, or onClick, but not variant or size. The dollar sign tells every reader that the prop exists only to feed the CSS.
Typing transient props without polluting DOM attribute types
In TypeScript, keep transient props separate from the standard HTML attribute interface so consumers get autocomplete without implying the prop is a real DOM attribute. For example, do not merge $variant into React.ButtonHTMLAttributes<HTMLButtonElement>; declare it in a dedicated type and pass that type to styled.button. The exact ergonomics vary by styled-components types and React version, so confirm that your installed release excludes transient props from DOM attribute typings before relying on it.
| Scenario | Prop forwarding | Transient prop behavior |
|---|---|---|
styled.button | Filters invalid HTML attributes | $ props consumed, not forwarded |
styled(MyComponent) | Forwards all props to MyComponent | $ props consumed, not forwarded to MyComponent |
Trade-offs and where the convention stops
Transient props are a naming convention and forwarding rule, not a public API contract or a security boundary. They only change what styled-components forwards; they do not stop a parent component or a custom child from choosing to do something with a prop. If a value later needs to reach a wrapped component, analytics, or tests, pass a separate non-$ prop or use shouldForwardProp for conditional forwarding.
The convention also does not address CSS specificity, global CSS, theming, or server-side rendering. Use it alongside a theming strategy and an SSR setup appropriate for your framework rather than as a general styling solution. Finally, do not use transient props to hide semantically meaningful values; accessibility and HTML validity still require real attributes such as aria-*, type, or disabled.
A local verification checklist
Before standardizing the rule, confirm the behavior against your installed versions. styled-components prop filtering and transient-prop behavior can differ across majors and React versions.
- Run
npm ls styled-components reactin your project root and record the exact versions. - In a scratch app pinned to those versions, render the
Buttonand inspect the DOM to confirm$variantand$sizedo not appear as attributes. - Compare
styled.buttonwithstyled(MyComponent)in the same scratch app; forwarding behavior is not identical. - Add a compile-time check for the
Buttonprop types and a runtime DOM assertion in your test runner. Avoid asserting generated class names because they are unstable.
A simple runtime assertion pattern looks like this:
// Adapt to your test runner
const { getByRole } = render(<Button $variant="primary" $size="sm">Save</Button>);
const button = getByRole('button');
expect(button).not.toHaveAttribute('variant');
expect(button).not.toHaveAttribute('size');
Adopt the rule deliberately
Make $ props styling-only and unprefixed props behavior/data. Start with new components, migrate the noisiest prop-forwarding cases first, and verify with a local DOM assertion. The goal is not to eliminate props; it is to make the boundary between presentation inputs and component behavior explicit enough that the next engineer does not have to guess.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.