Actionable Rules for building scalable, high-performance styles with CSS-in-JS (styled-components, Emotion, Linaria, vanilla-extract, styled-jsx) in a TypeScript + React/Next.js codebase.
Break free from global stylesheet chaos and component styling guesswork. These Cursor Rules transform how you build scalable, maintainable styles in modern React applications.
You're already dealing with these daily frustrations:
!important declarations and wondering why styles don't applyThese aren't just styling problems—they're productivity killers that compound as your codebase grows.
This rule set provides battle-tested patterns for every major CSS-in-JS library, from runtime solutions like styled-components to zero-runtime tools like vanilla-extract. You get consistent, scalable patterns whether you're building component libraries or shipping production applications.
Unified Development Patterns: Write consistent, predictable styles across styled-components, Emotion, Linaria, vanilla-extract, and styled-jsx with the same mental model.
Performance-First Architecture: Clear guidance on when to use static extraction vs. runtime libraries, with concrete bundle size targets and optimization strategies.
TypeScript-Native Styling: Full type safety for theme tokens, dynamic props, and component APIs—catch styling errors at compile time.
Zero Context-Switching Workflow: Co-located styles, clear file organization, and modular style exports that keep you in the zone.
// Before: Scattered styles
// components/Card.tsx + styles/card.scss + themes/variables.scss
// After: Everything together
const Card = styled.section<{ $variant: 'primary' | 'secondary' }>`
padding: ${({ theme }) => theme.spacing(3)};
background: ${({ theme, $variant }) => theme.colors[$variant]};
`;
export const ProductCard: React.FC<Props> = ({ title, price }) => (
<Card $variant="primary">
<Title>{title}</Title>
<Price>{price}</Price>
</Card>
);
// Static extraction with vanilla-extract
export const cardStyles = style({
padding: vars.spacing.lg,
borderRadius: vars.borderRadius.md,
// Compiled to CSS at build time - zero runtime cost
});
// Runtime styling only when truly dynamic
const DynamicCard = styled.div<{ $userColor: string }>`
${cardStyles}; // Compose static styles
border-color: ${({ $userColor }) => $userColor}; // Dynamic only when needed
`;
interface Theme {
colors: Record<'primary' | 'secondary' | 'error', string>;
spacing: (factor: number) => string;
}
// TypeScript catches invalid tokens at compile time
const Button = styled.button`
color: ${({ theme }) => theme.colors.primary}; // ✅ Valid
color: ${({ theme }) => theme.colors.invalid}; // ❌ TypeScript error
`;
The Challenge: Creating reusable, themeable components that work across different projects.
The Solution:
// components/button/Button.styles.ts
const baseButton = css`
display: inline-flex;
align-items: center;
justify-content: center;
border: none;
cursor: pointer;
transition: all 0.2s ease;
`;
export const Button = styled.button<{
$variant?: 'primary' | 'secondary';
$size?: 'sm' | 'lg';
}>`
${baseButton};
padding: ${({ $size = 'sm' }) => ($size === 'lg' ? '12px 20px' : '8px 16px')};
background: ${({ theme, $variant = 'primary' }) => theme.colors[$variant]};
&:hover {
transform: translateY(-1px);
}
@media (prefers-reduced-motion: reduce) {
transition: none;
&:hover { transform: none; }
}
`;
// components/button/Button.tsx
export const Button: React.FC<ButtonProps> = ({ children, variant, size, ...props }) => (
<StyledButton $variant={variant} $size={size} {...props}>
{children}
</StyledButton>
);
Impact: One component definition works across your entire application with full type safety and accessibility built in.
The Challenge: CSS-in-JS libraries adding unnecessary runtime overhead to your production bundle.
The Solution:
// Use static extraction for predictable styles
// styles/shared.css.ts (vanilla-extract)
export const flexCenter = style({
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
});
export const visuallyHidden = style({
position: 'absolute',
width: '1px',
height: '1px',
padding: 0,
margin: '-1px',
overflow: 'hidden',
clip: 'rect(0, 0, 0, 0)',
whiteSpace: 'nowrap',
border: 0,
});
// Runtime CSS-in-JS only for truly dynamic styles
const UserAvatar = styled.img<{ $online: boolean }>`
${flexCenter}; // Static styles compiled at build time
border: 2px solid ${({ $online, theme }) =>
$online ? theme.colors.success : theme.colors.gray
}; // Dynamic only when necessary
`;
Result: 15% reduction in JavaScript bundle size while maintaining styling flexibility.
The Challenge: Multiple developers working on the same component library with different styling approaches.
The Solution:
// Shared conventions across the team
// src/styles/primitives.ts
export const createFlexBox = (direction: 'row' | 'column' = 'row') => css`
display: flex;
flex-direction: ${direction};
`;
export const createSpacing = (factor: number) => css`
padding: ${({ theme }) => theme.spacing(factor)};
`;
// Usage is consistent across all team members
const Layout = styled.div`
${createFlexBox('column')};
${createSpacing(4)};
gap: ${({ theme }) => theme.spacing(2)};
`;
Impact: Consistent patterns, easier code reviews, and faster onboarding for new team members.
Install the rule set and configure your development environment:
# Install your preferred CSS-in-JS library
npm install styled-components @types/styled-components
# OR
npm install @emotion/react @emotion/styled
# Install linting tools
npm install --save-dev eslint-plugin-styled-components-a11y stylelint
// src/theme/index.ts
export interface Theme {
colors: Record<'primary' | 'secondary' | 'error' | 'gray', string>;
spacing: (factor: number) => string;
breakpoints: Record<'sm' | 'md' | 'lg', string>;
}
export const theme: Theme = {
colors: {
primary: '#3b82f6',
secondary: '#8b5cf6',
error: '#ef4444',
gray: '#6b7280',
},
spacing: (factor) => `${factor * 4}px`,
breakpoints: {
sm: '640px',
md: '768px',
lg: '1024px',
},
};
src/
├── components/
│ └── card/
│ ├── Card.tsx # React component
│ ├── Card.styles.ts # Styled components
│ ├── Card.test.tsx # Tests with style assertions
│ └── index.ts # Barrel export
├── styles/
│ ├── primitives.ts # Reusable style functions
│ ├── theme.ts # Theme configuration
│ └── global.ts # Global styles and resets
// Card.test.tsx
import { render } from '@testing-library/react';
import { ThemeProvider } from 'styled-components';
import { Card } from './Card';
import { theme } from '../../styles/theme';
describe('Card Component', () => {
it('applies correct styles for primary variant', () => {
const { container } = render(
<ThemeProvider theme={theme}>
<Card variant="primary">Content</Card>
</ThemeProvider>
);
expect(container.firstChild).toHaveStyle({
backgroundColor: theme.colors.primary,
padding: theme.spacing(3),
});
});
});
Before: Mixed styling approaches, frequent bugs, slow feature development After: Consistent patterns, compile-time safety, accelerated development
These rules don't just organize your styles—they transform how your team builds user interfaces. Get started today and experience the difference systematic CSS-in-JS patterns make in your development workflow.
You are an expert in React, Next.js, TypeScript, and modern CSS-in-JS libraries (styled-components, Emotion, Linaria, vanilla-extract, styled-jsx).
Key Principles
- Co-locate component code and styles in the same file (or side-by-side index.tsx / styles.ts) to minimise context-switching.
- Treat styles as first-class modules: export them, import them, and test them.
- Prefer static extraction (Linaria, vanilla-extract) for production bundles; fall back to runtime libraries only when dynamic calculations are essential.
- Scope every class name with the library’s hashing mechanism; never rely on global selectors except for reset, typography, or brand tokens.
- Keep selectors shallow (max 2 levels deep, e.g. `& > button`) to avoid specificity wars and improve override flexibility.
- Theming must go through a central Theme object – never prop-drill colour tokens.
- Favour style composability over duplication: extract reusable primitives (e.g. `flexCenter`, `visuallyHidden`).
- Document every design-system token (spacing, colour, typography) in code and Storybook.
TypeScript / JavaScript Rules
- Use React Function Components with explicit React.FC<Props> typing.
- Export a named `StyledX` object for each styled component: `export const Card = styled.section`….
- Put style declarations above component logic: 1) imports 2) styled components 3) React component.
- For dynamic styles, keep the function body pure and O(1):
```ts
const Button = styled.button<{ $size?: 'sm' | 'lg' }>`
padding: ${({ $size = 'sm' }) => ($size === 'lg' ? '12px 20px' : '8px 16px')};
`;
```
- Never create inline `{ style: { … }}` objects inside JSX; move them to CSS-in-JS blocks to avoid re-renders.
- File naming: `*.styles.ts` or colocated tagged template blocks inside `*.tsx` files.
- Run ESLint with `@emotion/eslint-plugin` / `eslint-plugin-styled-components-a11y` for linting.
Error Handling and Validation
- Enable TypeScript strict mode so invalid theme keys throw compile-time errors.
- Install `stylelint` with the `stylelint-config-recommended-scss` + the CSS-in-JS parser to catch invalid CSS.
- Integrate `eslint-plugin-import-helpers` to prevent forgotten stylesheet imports.
- Validate dynamic props: guard against undefined tokens – e.g. `assert(theme.colors[key], 'Unknown colour token')`.
- Implement a CI step that fails the build on stylelint or bundle-size regressions.
React + CSS-in-JS
- Use functional components; never mutate styled component props after first render.
- Compose styles via interpolations rather than nested selectors:
```ts
const accentBorder = css`border: 2px solid ${theme.colors.accent};`;
const Box = styled.div`${accentBorder}; padding: 24px;`;
```
- Create a `ThemeProvider` at the app root. Theme shape:
```ts
interface Theme {
colors: Record<'primary' | 'accent' | 'error', string>;
spacing: (factor: number) => string; // e.g. (2) => '8px'
}
```
- In Next.js, hydrate critical styles with `<GlobalStyles />` or `ServerStyleSheet` to avoid FOUC.
- Always forward refs with `styled(Component)` when creating wrapped components.
Library-Specific Rules
styled-components
- Prefix transient props with `$` (e.g. `$variant`) so they don’t leak to DOM.
- Use `.attrs` only for static props; never compute them with side-effects.
Emotion
- Prefer the `css` prop for quick ad-hoc styles; migrate to extracted `styled` when size grows.
- Enable `@emotion/babel-plugin` for labelled class names in dev and minified hashes in prod.
Linaria / vanilla-extract
- Keep expressions serialisable (no closures) so they can be evaluated at build time.
- Use design tokens via vanilla-extract’s `createTheme` and compile them to CSS variables.
styled-jsx
- Limit usage to Next.js pages; isolate component-local styles with `<style jsx>`.
- Use `css.resolve` for style reuse across components.
Testing
- Jest + React Testing Library: assert on rendered class names through `toHaveStyle` or `toMatchInlineSnapshot()`.
- Percy / Chromatic for visual regression; fail PRs on unexpected diffs.
- Storybook: provide `Dark` / `Light` theme stories and run `@storybook/addon-a11y`.
Performance Optimisation
- Audit bundle size weekly with `webpack-bundle-analyzer`; keep CSS-in-JS chunk <15% of JS payload.
- Prefer `styled.div` over `styled("div")` to enable minifier property-based tree-shaking.
- Memoise heavy dynamic style functions with `useMemo` if they depend on large objects.
- Purge unused styles with `@linaria/webpack5-loader` or Next.js’ built-in SWC shake.
Security & Accessibility
- Never interpolate unsanitised user input into CSS – escape or whitelist.
- Enforce `eslint-plugin-jsx-a11y` and `eslint-plugin-styled-components-a11y`.
- Provide `prefers-reduced-motion` variants for animations:
```css
@media (prefers-reduced-motion: reduce) { animation: none; }
```
Documentation & Tooling
- Generate a Theme JSON schema and publish via Docs site for designers.
- Add IntelliSense support with `typescript-styled-plugin`.
- Maintain a shared Figma ↔ code token source of truth via `tokens-studio` JSON.
Directory Convention Example
```
src/
└─ components/
└─ card/
├─ Card.tsx // React component
├─ Card.styles.ts // styled-components & css helpers
├─ Card.test.tsx // behaviour + style snapshot
└─ index.ts // barrel export
```
Cheat Sheet – Do & Don’t
- ✅ `const Danger = styled.span`color: ${({ theme }) => theme.colors.error};`;
- ❌ `style={{ color: theme.colors.error }}` inside JSX.
- ✅ `&:focus-visible { outline: 2px solid currentColor; }`
- ❌ `& .child & .grandchild { … }` overly specific nesting.
Follow these rules to produce fast, maintainable, and predictable styling with zero runtime surprises.