npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@alex.radulescu/styled-static

v0.7.3

Published

Near-zero-runtime styled components for React 19+ with Vite. CSS extracted at build time, minimal runtime.

Downloads

320

Readme

styled-static

Near-zero-runtime CSS-in-JS for React 19+ with Vite. Write styled-components syntax, get static CSS extracted at build time.

What's "zero"? CSS generation happens at build time (the expensive part). A minimal runtime (~45 bytes) handles className merging. Components are generated inline at build time.

Features

  • Static CSS - All CSS extracted at build time, no runtime stylesheet generation
  • 🎯 Type-Safe - Full TypeScript support with proper prop inference
  • 🎨 Familiar API - styled-components syntax you already know
  • 📦 Tiny - Minimal ~45 byte runtime for className merging only
  • 🔧 Zero Dependencies - Uses native CSS features and Vite's built-in tools
  • 🌳 Inline Components - Components generated at build time, no runtime factories
  • 🌓 Theme Helpers - Simple utilities for dark mode and custom themes

Quick Overview

All the APIs you need at a glance. styled-static provides 10 core functions that cover most CSS-in-JS use cases:

styled.element

Style HTML elements with template literals:

const Button = styled.button`
  padding: 0.5rem 1rem;
  ...
`;

const PrimaryButton = styled(Button)`
  font-weight: bold;
  ...
`;

const activeClass = css`
  outline: 2px solid blue;
  ...
`;

<Button className={isActive ? activeClass : ""}>Click</Button>;

const GlobalStyle = createGlobalStyle`
  * { box-sizing: border-box; }
  body { margin: 0; font-family: system-ui; }
`;

<GlobalStyle />; // Render once at app root

// With css`` for IDE syntax highlighting (recommended)
const Button = styledVariants({
  component: "button",
  css: css`
    padding: 0.5rem 1rem;
    border-radius: 4px;
  `,
  variants: {
    size: {
      sm: css`
        font-size: 0.875rem;
      `,
      lg: css`
        font-size: 1.125rem;
      `,
    },
  },
});

<Button size="lg">Large Button</Button>;

const badgeCss = cssVariants({
  css: css`
    padding: 0.25rem 0.5rem;
    border-radius: 4px;
  `,
  variants: {
    color: {
      blue: css`
        background: #e0f2fe;
        color: #0369a1;
      `,
      green: css`
        background: #dcfce7;
        color: #166534;
      `,
    },
  },
});

<span className={badgeCss({ color: "blue" })}>Info</span>;

// Combine classes conditionally
<div className={cx("base", isActive && activeClass)} />;

// Default attributes
const PasswordInput = styled.input.attrs({ type: "password" })`
  padding: 0.5rem 1rem;
`;

// Polymorphism - render Link with Button's styles
import { Link } from "react-router-dom";
const LinkButton = withComponent(Link, Button);
<LinkButton to="/path">Router link styled as button</LinkButton>;

Table of Contents


Why styled-static?

  • 🌐 CSS evolved. Native nesting, CSS variables, container queries—the gap between CSS and CSS-in-JS is smaller than ever.
  • 😵 CSS-in-JS fatigue. Most libraries are obsolete, complex, or have large runtime overhead.
  • Syntactic sugar over CSS modules. Better DX for writing CSS, without runtime interpolation.
  • 🔒 Zero dependencies. Minimal attack surface. Nothing to audit.
  • 🎯 Intentionally simple. 95% native browser + 5% sprinkles.
  • 🎉 Built for fun. Curiosity-driven, useful code.

What We Don't Do

  • 🚫 No runtime interpolation — Can't write ${props => props.color}. Use variants, CSS variables, or data attributes.
  • ⚛️ React 19+ only — Uses automatic ref forwarding (no forwardRef).
  • Vite only — Uses Vite's AST parser and virtual modules. No Webpack/Rollup.
  • 🚫 No css prop — Use named css variables with className.
  • 🚫 No shouldForwardProp — Not needed. Variants auto-strip props.

Each constraint removes complexity—no CSS parsing, no forwardRef, one great integration.


Installation

npm install @alex.radulescu/styled-static
# or
bun add @alex.radulescu/styled-static

Configure the Vite plugin:

// vite.config.ts
import react from "@vitejs/plugin-react";
import { styledStatic } from "@alex.radulescu/styled-static/vite";
import { defineConfig } from "vite";

export default defineConfig({
  plugins: [styledStatic(), react()],
});

Note: The plugin must be placed before the React plugin in the plugins array.


API Reference

styled

Create styled React components:

import { styled } from "@alex.radulescu/styled-static";

const Button = styled.button`
  padding: 0.5rem 1rem;
  background: #3b82f6;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;

  &:hover {
    background: #2563eb;
  }
`;

// Usage
<Button onClick={handleClick}>Click me</Button>;

Component Extension

Extend existing styled components by passing them to styled():

const Button = styled.button`
  padding: 0.5rem 1rem;
  border-radius: 4px;
`;

// Extend with additional styles
const PrimaryButton = styled(Button)`
  background: #3b82f6;
  color: white;
`;

// Chain extensions
const LargePrimaryButton = styled(PrimaryButton)`
  padding: 1rem 2rem;
  font-size: 1.25rem;
`;

CSS Cascade Order: When components are extended, classes are ordered correctly:

  • Base styles first
  • Extension styles second (override base)
  • User className last (override all)
<LargePrimaryButton className="custom" />
// Renders: class="ss-base ss-primary ss-large custom"

css Helper

Get a scoped class name for mixing with other classes:

import { css } from '@alex.radulescu/styled-static';

const activeClass = css`
  outline: 2px solid blue;
`;

const highlightClass = css`
  box-shadow: 0 0 10px yellow;
`;

// Mix with styled components
<Button className={isActive ? activeClass : ''}>
  Conditional styling
</Button>

// Combine multiple classes
<div className={`${activeClass} ${highlightClass}`}>
  Multiple classes
</div>

keyframes

Create scoped keyframe animations. The animation name is hashed to avoid conflicts between components:

import { keyframes, styled } from "@alex.radulescu/styled-static";

const spin = keyframes`
  from { transform: rotate(0deg); }
  to { transform: rotate(360deg); }
`;

const pulse = keyframes`
  0%, 100% { opacity: 1; }
  50% { opacity: 0.5; }
`;

const Spinner = styled.div`
  width: 24px;
  height: 24px;
  border: 2px solid #3b82f6;
  border-top-color: transparent;
  border-radius: 50%;
  animation: ${spin} 1s linear infinite;
`;

const PulsingDot = styled.div`
  width: 8px;
  height: 8px;
  background: #10b981;
  border-radius: 50%;
  animation: ${pulse} 2s ease-in-out infinite;
`;

Animation names are hashed at build time to avoid conflicts.

attrs

Set default HTML attributes using .attrs():

const SubmitButton = styled.button.attrs({
  type: 'submit',
  'aria-label': 'Submit form',
})`
  padding: 0.5rem 1rem;
  background: #3b82f6;
  color: white;
`;

<SubmitButton>Send</SubmitButton>
// Renders: <button type="submit" aria-label="Submit form" class="ss-xyz789">

Note: attrs must be static objects (no functions). For dynamic attributes, use regular props.

cx Utility

Combine class names conditionally. Intentionally flat (no nested arrays/objects) for minimal bundle size:

import { css, cx } from '@alex.radulescu/styled-static';

const activeClass = css`color: blue;`;

cx('base', 'active')                    // → "base active"
cx('btn', isActive && activeClass)      // → "btn ss-abc123" or "btn"
cx('a', null, undefined, false, 'b')    // → "a b"

Global Styles

import { createGlobalStyle } from "@alex.radulescu/styled-static";

const GlobalStyle = createGlobalStyle`
  * {
    box-sizing: border-box;
  }

  body {
    margin: 0;
    font-family: system-ui, sans-serif;
  }

  :root {
    --color-primary: #3b82f6;
    --color-text: #1a1a1a;
  }
`;

// Render once at app root
createRoot(document.getElementById("root")!).render(
  <StrictMode>
    <GlobalStyle />
    <App />
  </StrictMode>
);

Variants API

For type-safe variant handling, use styledVariants to create components with variant props, or cssVariants to get class functions.

Tip: Wrap CSS strings in css\...`` to get IDE syntax highlighting from the styled-components VSCode extension.

styledVariants

import { css, styledVariants } from "@alex.radulescu/styled-static";

const Button = styledVariants({
  component: "button",
  css: css`
    padding: 0.5rem 1rem;
    background: gray;
    color: white;
    font-size: 1rem;
  `,
  variants: {
    color: {
      primary: css`background: blue;`,
      danger: css`background: red;`,
      success: css`background: green;`,
    },
    size: {
      sm: css`font-size: 0.875rem; padding: 0.25rem 0.5rem;`,
      lg: css`font-size: 1.125rem; padding: 0.75rem 1.5rem;`,
    },
  },
});

<Button color="primary" size="lg">Click me</Button>
// Renders: <button class="ss-abc ss-abc--color-primary ss-abc--size-lg">

cssVariants

import { cssVariants, css, cx } from '@alex.radulescu/styled-static';

// With css`` for syntax highlighting (recommended)
const badgeCss = cssVariants({
  css: css`
    padding: 0.25rem 0.5rem;
    border-radius: 4px;
    font-size: 0.75rem;
  `,
  variants: {
    variant: {
      info: css`background: #e0f2fe; color: #0369a1;`,
      success: css`background: #dcfce7; color: #166534;`,
      warning: css`background: #fef3c7; color: #92400e;`,
    },
  },
});

// Usage - returns class string
<span className={badgeCss({ variant: 'info' })}>Info</span>
// Returns: "ss-xyz ss-xyz--variant-info"

// Combine with cx for conditional classes
<span className={cx(badgeCss({ variant: 'info' }), isActive && activeClass)}>
  Info
</span>

Features

Polymorphism with withComponent

Render one component with another's styles using withComponent:

import { Link } from "react-router-dom";
import { styled, withComponent } from "@alex.radulescu/styled-static";

const Button = styled.button`
  padding: 0.5rem 1rem;
  background: blue;
  color: white;
`;

// Create a Link that looks like Button
const LinkButton = withComponent(Link, Button);

// Also works with HTML tags
const AnchorButton = withComponent('a', Button);

// Usage
<LinkButton to="/path">Router link styled as button</LinkButton>
<AnchorButton href="/external">External link</AnchorButton>

withComponent accepts:

  • First argument: The component to render (React component or HTML tag string)
  • Second argument: The styled component whose styles to use

Manual Composition with .className

Every styled component exposes a static .className property for manual composition:

const Button = styled.button`
  padding: 0.5rem 1rem;
  background: blue;
`;

// Use className directly on any element
<a className={Button.className} href="/link">
  Link with button styles
</a>

// Combine with cx utility
<div className={cx(Button.className, Card.className, "custom")}>
  Combined styles
</div>

This is useful when you need button styles on a non-component element or want to combine multiple styled component classes.

CSS Nesting

styled-static uses native CSS nesting (supported in all modern browsers):

const Card = styled.div`
  padding: 1rem;
  background: white;
  border-radius: 8px;

  /* Pseudo-classes */
  &:hover {
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
  }

  /* Child selectors */
  & h2 {
    margin: 0 0 0.5rem;
  }

  /* Media queries */
  @media (max-width: 640px) {
    padding: 0.5rem;
  }

  /* Pseudo-elements */
  &::before {
    content: "";
    position: absolute;
  }
`;

Tip: Native CSS nesting means zero build-time processing. Your CSS is passed directly to the browser.


Dynamic Styling

No runtime interpolation—use these patterns instead:

  • Variants API — Type-safe component variants (recommended)
  • cx utility — Conditional class toggling
  • CSS variables — Pass via style prop for truly dynamic values
  • Data attributes — Style with &[data-variant="x"] selectors

Theming

CSS-first theming with CSS variables and data-theme attributes:

const GlobalStyle = createGlobalStyle`
  :root, [data-theme="light"] { --bg: #fff; --text: #1a1a1a; }
  [data-theme="dark"] { --bg: #0a0a0a; --text: #f1f5f9; }
  [data-theme="pokemon"] { --bg: #ffcb05; --text: #2a75bb; }
`;

const Card = styled.div`
  background: var(--bg);
  color: var(--text);
`;

Theme Helpers

import { initTheme, setTheme, getTheme, onSystemThemeChange } from "@alex.radulescu/styled-static";

// Initialize (reads localStorage → system preference → default)
initTheme({ defaultTheme: "light", useSystemPreference: true });

// Switch themes
setTheme("dark");              // persists to localStorage
setTheme("pokemon", false);    // no persist (preview)

// Read current
const current = getTheme();    // 'light' | 'dark' | etc.

// React to OS changes
const unsub = onSystemThemeChange((prefersDark) => {
  if (!localStorage.getItem("theme")) setTheme(prefersDark ? "dark" : "light", false);
});

| Function | Description | | -------- | ----------- | | initTheme(options?) | Init on load. Priority: localStorage → system → default | | setTheme(theme, persist?) | Set theme. Persists to localStorage by default | | getTheme() | Get current theme from data-theme | | onSystemThemeChange(cb) | Subscribe to OS theme changes |


Troubleshooting

Storybook: "This package is ESM only"

If you see this error when using styled-static with Storybook:

Failed to resolve "@alex.radulescu/styled-static/vite".
This package is ESM only but it was tried to load by `require`.

Add the package to Vite's optimizeDeps.include in your Storybook config:

// .storybook/main.ts
export default {
  // ... other config
  viteFinal: async (config) => {
    config.optimizeDeps = config.optimizeDeps || {};
    config.optimizeDeps.include = [
      ...(config.optimizeDeps.include || []),
      '@alex.radulescu/styled-static',
    ];
    return config;
  },
};

This is a known limitation with ESM-only packages in Storybook's esbuild-based config loading.


How It Works

styled-static uses a Vite plugin to transform your styled components at build time. Here's what happens under the hood:

Build-Time Transformation

When you write a styled component, the Vite plugin intercepts your code and performs AST-based transformation:

// 1. What you write:
import { styled } from "@alex.radulescu/styled-static";

const Button = styled.button`
  padding: 1rem;
  background: blue;
  color: white;
`;

// 2. What gets generated:
import { createElement } from "react";
import { m } from "@alex.radulescu/styled-static/runtime";
import "@alex.radulescu/styled-static:abc123-0.css";

const Button = Object.assign(
  (p) => createElement("button", {...p, className: m("ss-abc123", p.className)}),
  { className: "ss-abc123" }
);

The CSS is completely removed from your JavaScript bundle and extracted to a virtual CSS module. The component becomes an inline function with a static .className property for composition.

Virtual CSS Modules

Each styled component gets its own virtual CSS module with a unique ID like styled-static:abc123-0.css. This approach enables:

  • Deduplication - CSS is optimized by Vite's pipeline
  • Code splitting - CSS loads only with the components that use it
  • Hot Module Replacement - Changes to styles trigger instant HMR
  • Production optimization - CSS can be extracted to a single file
/* Virtual module: styled-static:abc123-0.css */
.ss-abc123 {
  padding: 1rem;
  background: blue;
  color: white;
}

Minimal Runtime

The runtime is extremely small because components are generated inline at build time. The only runtime code is a className merge helper:

| Module | Minified | Brotli | | ---------------- | -------- | ------ | | runtime/index.js | 45 B | 50 B |

This is a 98% reduction from traditional CSS-in-JS libraries.

// The ENTIRE runtime - just className merging
export const m = (base, user) => user ? `${base} ${user}` : base;

Everything else is generated at build time as inline components.

Zero-Runtime Features

Some features have literally zero runtime cost because they're completely replaced at build time:

// css helper - zero runtime (just a string)
const activeClass = css`outline: 2px solid blue;`;
// Generated: const activeClass = "ss-xyz789";

// Global styles - zero runtime (just CSS import)
const GlobalStyles = createGlobalStyle`* { box-sizing: border-box; }`;
// Generated: const GlobalStyles = () => null;

// withComponent - zero runtime (build-time transformation)
const LinkButton = withComponent(Link, Button);
// Generated: Object.assign((p) => createElement(Link, {...p, className: m(Button.className, p.className)}), { className: Button.className })

Configuration

styledStatic({
  // Prefix for generated class names (default: 'ss')
  classPrefix: "my-app",

  // CSS output mode (default: 'auto')
  // - 'auto': Uses 'file' for library builds (build.lib set), 'virtual' for apps
  // - 'virtual': CSS as virtual modules (Vite bundles into single file)
  // - 'file': CSS as separate files co-located with JS (for library builds)
  cssOutput: "auto",
});

Library Builds

When building a component library with build.lib configured, styled-static automatically outputs CSS as separate files co-located with each JS file. This enables CSS tree-shaking for consuming applications.

dist/
  components/
    Button/
      Button.js    # imports "./Button.css"
      Button.css   # Button-specific styles only
    Alert/
      Alert.js     # imports "./Alert.css"
      Alert.css    # Alert-specific styles only

Consuming apps automatically get only the CSS for components they import:

// In your app - only Button.css is included in the bundle
import { Button } from "my-component-library/components/Button";

For app builds (no build.lib), CSS is bundled as virtual modules into a single CSS file, which is the default Vite behavior.


TypeScript

Full type inference is provided:

const Button = styled.button`...`;

// ✅ Type-safe: button props are available
<Button type="submit" disabled>Submit</Button>

// ✅ Type-safe: withComponent infers props from target component
const LinkButton = withComponent(Link, Button);
<LinkButton to="/path">Link</LinkButton>

// ✅ Type-safe: .className is always string
const classes = Button.className; // string

Zero Dependencies

Zero runtime dependencies. Uses native CSS nesting (Chrome 112+, Safari 16.5+, Firefox 117+) and Vite's CSS pipeline. See Installation for optional Lightning CSS integration.


Comparison

Legend: ✓ Yes | ◐ Partial | ✗ No

| | styled-static | Emotion | Linaria | Restyle | Panda CSS | |-|---------------|---------|---------|--------|-----------| | Runtime | ~50 B | ~11 KB | ~1.5 KB | ~2.2 KB | 0 B | | Dependencies | 0 | 5+ | 10+ | 0 | 5+ | | React | 19+ | 16+ | 16+ | 19+ | 16+ | | Bundler | Vite | Any | Many | Any | Any | | styled.el | ✓ | ✓ | ✓ | ✓ | ◐ | | styled(Comp) | ✓ | ✓ | ✓ | ✓ | ◐ | | Variants | ✓ | ◐ | ◐ | ◐ | ✓ | | css helper | ✓ | ✓ | ✓ | ✓ | ✓ | | css inline prop | ✗ | ✓ | ✗ | ✓ | ✓ | | Runtime interpolation | ✗ | ✓ | ✗ | ✓ | ✗ | | .className access | ✓ | ✗ | ✗ | ✗ | ✗ |

When to choose: styled-static for familiar DX + zero deps + React 19/Vite. Emotion for runtime interpolation + ThemeProvider. Linaria for multi-bundler zero-runtime. Restyle for css prop + Server Components. Panda for atomic CSS + design tokens.


VS Code Support

For syntax highlighting in template literals, install the vscode-styled-components extension.


Inspiration

We take inspiration from the greats before us: Emotion, styled-components, Linaria, Panda CSS, Pigment CSS, Stitches, Ecsstatic, Restyle, goober. Thanks to each and every one for ideas and inspiration.


License

MIT