Vitus Labs
Styler

Styler

High-performance CSS-in-JS engine for Vitus Labs UI System (~3KB gzipped).

@vitus-labs/styler is a lightweight CSS-in-JS engine replacing styled-components. It provides a familiar tagged template literal API with static/dynamic path optimization, automatic class name hashing, and React 19 SSR support.

Installation

npm install @vitus-labs/styler

Peer dependencies: react >= 19, react-dom >= 19

Key Features

  • ~3KB gzipped — Minimal bundle impact
  • Static/dynamic split — Templates without function interpolations compute class once at definition time (zero per-render cost)
  • FNV-1a hashing — Deterministic class names, automatic deduplication
  • CSS-in-CSS composition — Nest css results inside styled or other css calls
  • React 19 SSR<style precedence> on server, useInsertionEffect on client
  • Transient props$-prefixed props consumed by styles but never forwarded to DOM
  • Polymorphic as prop — Render as any element or component
  • Specificity boost — Double selector option for library component overrides
  • @layer support — CSS cascade layer wrapping
  • Bounded cache — Configurable max cache size with automatic eviction

Try It Live

function StyledDemo() {
const [color, setColor] = React.useState('#0d6efd')
const [radius, setRadius] = React.useState(8)

return (
  <div style={{ fontFamily: 'system-ui' }}>
    <div style={{ display: 'flex', gap: 12, marginBottom: 12 }}>
      <label style={{ display: 'flex', alignItems: 'center', gap: 6, fontSize: 14 }}>
        Color:
        <input type="color" value={color} onChange={(e) => setColor(e.target.value)} />
      </label>
      <label style={{ display: 'flex', alignItems: 'center', gap: 6, fontSize: 14 }}>
        Radius: {radius}px
        <input type="range" min={0} max={24} value={radius} onChange={(e) => setRadius(Number(e.target.value))} />
      </label>
    </div>
    <button style={{
      background: color,
      color: 'white',
      border: 'none',
      borderRadius: radius,
      padding: '10px 20px',
      fontSize: 14,
      cursor: 'pointer',
      fontWeight: 500,
    }}>
      Styled Button
    </button>
  </div>
)
}

render(<StyledDemo />)

css()

Tagged template for composable CSS fragments. Returns a lazy CSSResult object — no CSS resolution happens at creation time.

import { css } from '@vitus-labs/styler'

// Static — stored as-is, resolved when consumed
const highlight = css`
  color: red;
  font-weight: bold;
`

// Dynamic — function interpolations resolved at render time with props + theme
const dynamic = css`
  color: ${(props) => props.$color || 'blue'};
  padding: ${(props) => props.$size}px;
`

// Composition — nest css results inside other css
const combined = css`
  ${highlight}
  border: 1px solid gray;
`

// Array interpolation — flattened automatically
const responsive = css`
  ${[
    css`color: red;`,
    css`margin: 0;`,
  ]}
`
// Resolves to: "color: red;margin: 0;"

CSSResult

css() returns a CSSResult instance that stores the raw template strings and interpolation values:

class CSSResult {
  readonly strings: TemplateStringsArray
  readonly values: Interpolation[]

  // Resolve with empty props (useful for testing/debugging)
  toString(): string
}
  • No computation at creation time — only stores references
  • Resolved when consumed by styled, useCSS, or toString()
  • Instance check: value instanceof CSSResult detects nested CSS for composition
  • Thunk pattern: When used as interpolation before engine init, returns a thunk resolved at render

Interpolation Types

TypeBehavior
string / numberInserted directly into CSS
CSSResult (from another css call)Recursively resolved and flattened
Interpolation[] (array)Each element resolved and concatenated
(props) => InterpolationCalled at render time with { ...componentProps, theme }
true / falseConverted to empty string (enables ${condition && css\...`}`)
null / undefinedConverted to empty string
type Interpolation =
  | string
  | number
  | boolean
  | null
  | undefined
  | CSSResult
  | Interpolation[]   // recursive
  | ((props: { theme?: DefaultTheme; [key: string]: any }) => Interpolation)

CSS Normalization

Resolved CSS goes through a single-pass normalization that:

  • Strips /* block comments */ and // line comments (preserves :// in URLs)
  • Collapses whitespace to single spaces
  • Removes redundant semicolons (after {, }, other ;)

styled()

Component factory that creates styled React components with automatic class name management, ref forwarding, and prop filtering.

import { styled } from '@vitus-labs/styler'

// Tag shorthand via Proxy
const Box = styled.div`
  padding: 16px;
  border-radius: 8px;
`

// Direct call syntax
const Card = styled('section')`
  background: white;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
`

// Wrap existing component
const StyledButton = styled(MyButton)`
  cursor: pointer;
`

// With options
const Custom = styled('div', {
  shouldForwardProp: (prop) => !prop.startsWith('custom'),
  boost: true,
})`
  padding: 16px;
`

Options

styled(tag: string | ComponentType, options?: StyledOptions)
OptionTypeDefaultDescription
shouldForwardProp(prop: string) => booleanControl which props reach the DOM element
boostbooleanfalseDouble the selector (.vl-abc.vl-abc) for specificity (0,2,0)

Dynamic Props & Transient Props

Props prefixed with $ are automatically filtered from DOM output:

const Button = styled.button`
  background: ${(props) => props.$variant === 'primary' ? '#0d6efd' : '#6c757d'};
  color: white;
  padding: ${(props) => props.$size === 'large' ? '12px 24px' : '8px 16px'};
`

<Button $variant="primary" $size="large">Click me</Button>
// Renders: <button class="vl-abc123">Click me</button>
// $variant and $size consumed by styles, NOT forwarded to <button>

Polymorphic as Prop

Override the rendered element type at the call site:

const Box = styled.div`
  padding: 16px;
`

<Box as="section" />      // renders <section>
<Box as="article" />      // renders <article>
<Box as={MyComponent} />  // renders <MyComponent>

The as prop works in both static and dynamic paths and is never forwarded to the underlying element.

Ref Forwarding

All styled components forward refs via React.forwardRef:

const Input = styled.input`
  border: 1px solid gray;
`

const ref = useRef<HTMLInputElement>(null)
<Input ref={ref} />  // ref points to the <input> DOM element

shouldForwardProp

For custom prop filtering logic:

const Input = styled('input', {
  shouldForwardProp: (prop) => prop !== 'hasError',
})`
  border-color: ${(props) => props.hasError ? 'red' : 'gray'};
`

<Input hasError />
// hasError used by styles but NOT forwarded to <input>

shouldForwardProp only applies to DOM elements (string tags). When wrapping React components, all props are forwarded.

Default Prop Filtering

For DOM elements without custom shouldForwardProp:

Prop PatternForwarded?
$-prefixed (e.g., $color)No — transient props
asNo — consumed for polymorphism
data-*Yes
aria-*Yes
Known HTML attributesYes
Unknown propsNo — prevents React warnings

Class Name Merging

User-provided className is merged with the generated class:

<Box className="extra">Hello</Box>
// Renders: <div class="vl-abc123 extra">Hello</div>

Specificity Boost

Double the selector to raise specificity from (0,1,0) to (0,2,0):

const Override = styled('div', { boost: true })`
  color: red;
`
// Generates: .vl-abc.vl-abc { color: red; }
// Overrides inner library components regardless of CSS source order

Static vs Dynamic Paths

The engine automatically detects whether a template has dynamic interpolations and uses the optimal path:

Static Path

When no interpolation values are functions (or nested CSSResults containing functions):

  1. CSS resolved once at component creation time (module evaluation)
  2. Class name computed and CSS injected immediately
  3. Zero per-render overhead — no theme context access, no resolution
  4. Component simply applies the pre-computed class name
// Static — class computed once, shared across all instances
const Box = styled.div`
  padding: 16px;
  margin: 8px;
  background: white;
`

Dynamic Path

When any interpolation is a function:

  1. Theme accessed via useTheme() on every render
  2. CSS resolved with { ...componentProps, theme }
  3. useRef cache — CSS string comparison avoids rehashing when content unchanged
  4. useInsertionEffect injects CSS synchronously before paint
  5. Dedup cache prevents duplicate style injection
// Dynamic — resolved per render, cached by CSS string comparison
const Box = styled.div`
  padding: ${(props) => props.$p}px;
  color: ${(props) => props.theme.colors.primary};
`

keyframes()

Create CSS @keyframes animations. Injection is synchronous and immediate.

import { keyframes, styled } from '@vitus-labs/styler'

const fadeIn = keyframes`
  from { opacity: 0; transform: translateY(-10px); }
  to { opacity: 1; transform: translateY(0); }
`

const FadeInBox = styled.div`
  animation: ${fadeIn} 0.3s ease-in-out;
`

Behavior

  • Returns a KeyframesResult with a .name property (vl-kf-<hash>)
  • String coercion returns the animation name for use in CSS interpolations
  • Deterministic naming via FNV-1a hash of the CSS body
  • No dynamic interpolations — keyframes are always static
  • Injected as @keyframes vl-kf-<hash> { ... } into the global sheet
  • Deduplicated by animation name

createGlobalStyle()

Inject unscoped global CSS. Returns a React component.

import { createGlobalStyle } from '@vitus-labs/styler'

const GlobalStyles = createGlobalStyle`
  * {
    box-sizing: border-box;
    margin: 0;
  }

  body {
    font-family: system-ui, sans-serif;
    background: ${(props) => props.theme?.background || '#fff'};
  }
`

function App() {
  return (
    <>
      <GlobalStyles />
      <Main />
    </>
  )
}

Behavior

  • Returns a React component (not a CSSResult)
  • Component props are merged with theme for interpolation resolution
  • Unscoped — CSS injected without any .vl-* class wrapper
  • Static path: CSS resolved once at creation, component renders nothing on client
  • Dynamic path: CSS resolved per render, injected via useInsertionEffect
  • SSR: Renders <style precedence="low"> — lower precedence than component styles (medium)
  • Deduplicated by hash of resolved CSS

useCSS()

Hook that resolves a CSSResult to a class name string. Use when you need styling without creating a styled component wrapper.

import { css, useCSS } from '@vitus-labs/styler'

const highlight = css`
  color: red;
  font-weight: bold;
`

function Label({ children }) {
  const className = useCSS(highlight)
  return <span className={className}>{children}</span>
}

Signature

useCSS(
  template: CSSResult,
  props?: Record<string, any>,
  boost?: boolean
): string  // class name

Dynamic Styles

const badge = css`
  background: ${(props) => props.variant === 'success' ? 'green' : 'gray'};
  padding: 4px 8px;
  border-radius: 4px;
`

function Badge({ variant, children }) {
  const className = useCSS(badge, { variant })
  return <span className={className}>{children}</span>
}

Theme is automatically merged from context. Same caching and injection mechanism as styled().

CSS Nesting

Native CSS nesting with & selectors is fully supported:

const Card = styled.div`
  background: white;
  padding: 16px;

  & h2 {
    margin-bottom: 8px;
    font-size: 1.5rem;
  }

  & p {
    color: #666;
  }

  &:hover {
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
  }

  &.active {
    border-color: blue;
  }
`

At-Rule Splitting

@media, @supports, and @container at-rules are automatically extracted from the CSS body and emitted as separate top-level rules with the selector wrapped inside. This works around CSSOM spec limitations:

const Responsive = styled.div`
  font-size: 14px;
  @media (min-width: 600px) {
    font-size: 18px;
  }
`
// Emits two rules:
// .vl-abc { font-size: 14px; }
// @media (min-width: 600px) { .vl-abc { font-size: 18px; } }

Non-splittable at-rules (@keyframes, @font-face) are left in place.

Deduplication & Caching

  • Same CSS always produces the same FNV-1a hash → same class name
  • If two components generate identical CSS, the rule is injected once and both share the class name
  • Dynamic path uses a useRef cache per component instance — string comparison skips rehashing when CSS unchanged between renders
  • Global StyleSheet cache bounded by maxCacheSize (default 10,000) with 10% eviction on overflow

HMR Support

For development with hot module replacement:

import { sheet } from '@vitus-labs/styler'

if (import.meta.hot) {
  import.meta.hot.accept(() => {
    sheet.clearAll()  // Purge all cached + injected styles
  })
}
  • sheet.clearCache() — Clear dedup cache only (keep injected CSS)
  • sheet.clearAll() — Clear cache + ssrBuffer + remove all DOM rules

Benchmarks

All benchmarks run via Vitest bench. React is externalized in all bundle measurements.

Bundle Size

LibraryMinifiedGzipped
goober2.32 KB1.31 KB
@vitus-labs/styler10.13 KB3.81 KB
styled-components44.93 KB17.89 KB
@emotion/react + styled48.26 KB16.59 KB

Performance (ops/sec, higher is better)

Benchmarkstylerstyled-components@emotiongoober
css() creation25.2M9.0M2.2M26K
css() with interpolations24.9M5.6M2.3M28K
Template resolution21.4M3.9M
Dynamic interpolation12.4M13.4M
Nested composition8.3M2.2M1.4M8K
SSR renderToString307K69K192K18K
styled() factory17.3M109K933K18.2M

Styler is 2.8–1034x faster than alternatives across css creation, composition, and SSR. The styled() factory is essentially tied with goober (17.3M vs 18.2M ops/s) while being 158x faster than styled-components and 18x faster than Emotion. The only benchmark where styler doesn't lead is dynamic function interpolation, where styled-components' manual flatten is ~8% faster.

On this page