Skip to content

Component-scoped CSS with css prop #326

@brainkim

Description

@brainkim

Summary

Add a css prop for inline styles with support for nested selectors, pseudo-classes, and media queries. Styles are scoped to the component using the per-definition identifier (#325).

Prior Art

This proposal is directly inspired by Remix 3's css prop implementation.

Motivation

Current CSS-in-JS options require either:

  • Build-time transforms (vanilla-extract, Panda CSS)
  • Runtime libraries with large bundles (Emotion, styled-components)
  • Manual class name management

A built-in css prop would provide ergonomic scoped styles with zero dependencies.

Proposed API

function Button({primary}) {
  return (
    <button css={{
      color: 'white',
      background: primary ? 'blue' : 'gray',
      '&:hover': {
        opacity: 0.8,
      },
      '@media (max-width: 768px)': {
        padding: 8,  // auto-appends 'px'
      },
    }}>
      Click me
    </button>
  );
}

How it works

  1. Component has stable ID via per-definition identifier (Expose per-definition component identifier #325)
  2. CSS object is hashed to generate variant suffix
  3. Final class: ${componentId}-${variantHash} (e.g., c-7f3a-x9k2)
  4. CSS is generated and injected once per unique class
  5. Renderer manages style injection/deduplication with ref-counting

Features

  • Object syntax with camelCase properties
  • & for pseudo-selectors: &:hover, &:active, &::before
  • & for attribute selectors: &[disabled], &[aria-selected="true"]
  • Child selectors: .icon, > span
  • Media queries: @media (...)
  • @keyframes support
  • Auto px suffix for numeric values (except unitless props like z-index, opacity)

Why separate from style prop?

Keeping css and style as separate props allows:

  • style → always inline via CSSOM, fast updates, no stylesheet overhead
  • css → always generated class, supports pseudo/media, deduped

This gives control over rendering strategy. Use style for highly dynamic values (animations), css for static/scoped styles. Also keeps TypeScript types clean.

Implementation Sketch

~200 lines, runtime only:

  • processStyle(obj) - converts object to CSS string + hash
  • styleToCss(obj, selector) - handles nested selectors recursively
  • StyleManager - injects styles via adoptedStyleSheets, ref-counts for cleanup

Alternative: Raw Scoped Styles

The component ID also enables manual scoped CSS:

function Button() {
  return <>
    <button class={this.id}>Click me</button>
    <style>{`
      .${this.id} { color: white; background: blue; }
      .${this.id}:hover { background: darkblue; }
    `}</style>
  </>;
}

Open Questions

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions