@alex.radulescu/styled-static 0.2.0 → 0.7.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,12 +1,12 @@
1
1
  <!--
2
2
  ═══════════════════════════════════════════════════════════════════════════════
3
3
  This README serves as both user documentation and LLM context (LLMs.txt).
4
- It documents styled-static - a zero-runtime CSS-in-JS library for React 19+
5
- with Vite.
4
+ It documents styled-static - a near-zero-runtime CSS-in-JS library for React 19+
5
+ with Vite. CSS is extracted at build time; minimal runtime handles dynamic features.
6
6
 
7
7
  Key APIs: styled, css, createGlobalStyle, styledVariants, cssVariants, cx
8
8
  Theme helpers: initTheme, setTheme, getTheme, onSystemThemeChange
9
- Runtime: ~300 bytes | Dependencies: 0 | React 19+ required | Vite only
9
+ Runtime: Minimal | Dependencies: 0 | React 19+ required | Vite only
10
10
 
11
11
  For implementation details, see CLAUDE.md or the source files in src/
12
12
  ═══════════════════════════════════════════════════════════════════════════════
@@ -14,22 +14,25 @@ For implementation details, see CLAUDE.md or the source files in src/
14
14
 
15
15
  # styled-static
16
16
 
17
- Zero-runtime CSS-in-JS for React 19+ with Vite. Write styled-components syntax, get static CSS extracted at build time.
17
+ Near-zero-runtime CSS-in-JS for React 19+ with Vite. Write styled-components syntax, get static CSS extracted at build time.
18
+
19
+ **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.
18
20
 
19
21
  ## Features
20
22
 
21
- - ⚡ **Zero Runtime** - CSS extracted at build time, no CSS-in-JS overhead
23
+ - ⚡ **Static CSS** - All CSS extracted at build time, no runtime stylesheet generation
22
24
  - 🎯 **Type-Safe** - Full TypeScript support with proper prop inference
23
25
  - 🎨 **Familiar API** - styled-components syntax you already know
24
- - 📦 **Tiny** - ~300 bytes runtime for `as` prop and transient props support
26
+ - 📦 **Tiny** - Minimal ~45 byte runtime for className merging only
25
27
  - 🔧 **Zero Dependencies** - Uses native CSS features and Vite's built-in tools
28
+ - 🌳 **Inline Components** - Components generated at build time, no runtime factories
26
29
  - 🌓 **Theme Helpers** - Simple utilities for dark mode and custom themes
27
30
 
28
31
  ---
29
32
 
30
33
  ## Quick Overview
31
34
 
32
- All the APIs you need at a glance. styled-static provides 6 core functions that cover most CSS-in-JS use cases:
35
+ All the APIs you need at a glance. styled-static provides 10 core functions that cover most CSS-in-JS use cases:
33
36
 
34
37
  ### styled.element
35
38
 
@@ -101,55 +104,70 @@ const badgeCss = cssVariants({
101
104
  });
102
105
 
103
106
  <span className={badgeCss({ color: "blue" })}>Info</span>;
104
- ```
105
-
106
- ---
107
-
108
- ## Why styled-static?
109
107
 
110
- - 🌐 **CSS & browsers have evolved.** Native CSS nesting, CSS variables, container queries, and fewer vendor prefixes mean the gap between CSS and CSS-in-JS has never been smaller.
108
+ // Combine classes conditionally
109
+ <div className={cx("base", isActive && activeClass)} />;
111
110
 
112
- - 😵 **CSS-in-JS fatigue is real.** Most libraries are now obsolete, overly complex, or have large runtime overhead. The ecosystem needs simpler solutions.
111
+ // Default attributes
112
+ const PasswordInput = styled.input.attrs({ type: "password" })`
113
+ padding: 0.5rem 1rem;
114
+ `;
113
115
 
114
- - **Syntactic sugar over CSS modules.** Most projects don't need runtime interpolation. They need a better DX for writing and organizing CSS.
116
+ // Polymorphism - render Link with Button's styles
117
+ import { Link } from "react-router-dom";
118
+ const LinkButton = withComponent(Link, Button);
119
+ <LinkButton to="/path">Router link styled as button</LinkButton>;
120
+ ```
115
121
 
116
- - 🔒 **Supply chain security matters.** Zero dependencies means a minimal attack surface. No transitive dependencies to audit or worry about.
122
+ ---
117
123
 
118
- - 🎯 **Intentionally simple.** 95% native browser foundation + 5% sprinkles on top. We leverage what browsers already do well.
124
+ ## Table of Contents
119
125
 
120
- - 🎉 **Built for fun.** Sometimes the best projects come from curiosity and the joy of building something useful.
126
+ - [Quick Overview](#quick-overview) · [Why](#why-styled-static) · [What We Don't Do](#what-we-dont-do) · [Installation](#installation)
127
+ - **API:** [styled](#styled) · [Extension](#component-extension) · [css](#css-helper) · [keyframes](#keyframes) · [attrs](#attrs) · [cx](#cx-utility) · [Global Styles](#global-styles) · [Variants](#variants-api)
128
+ - **Features:** [Polymorphism](#polymorphism-with-withcomponent) · [.className](#manual-composition-with-classname) · [CSS Nesting](#css-nesting) · [Dynamic Styling](#dynamic-styling) · [Theming](#theming)
129
+ - **Internals:** [Troubleshooting](#troubleshooting) · [How It Works](#how-it-works) · [Config](#configuration) · [TypeScript](#typescript) · [Zero Deps](#zero-dependencies) · [Comparison](#comparison)
121
130
 
122
131
  ---
123
132
 
124
- ## What We Don't Do
133
+ ## Why styled-static?
125
134
 
126
- styled-static is intentionally limited. Here's what we don't support—and why:
135
+ - 🌐 **CSS evolved.** Native nesting, CSS variables, container queriesthe gap between CSS and CSS-in-JS is smaller than ever.
136
+ - 😵 **CSS-in-JS fatigue.** Most libraries are obsolete, complex, or have large runtime overhead.
137
+ - ✨ **Syntactic sugar over CSS modules.** Better DX for writing CSS, without runtime interpolation.
138
+ - 🔒 **Zero dependencies.** Minimal attack surface. Nothing to audit.
139
+ - 🎯 **Intentionally simple.** 95% native browser + 5% sprinkles.
140
+ - 🎉 **Built for fun.** Curiosity-driven, useful code.
127
141
 
128
- - 🚫 **No runtime interpolation.** You can't write `${props => props.color}`. CSS is extracted at build time, so values must be static. Use CSS variables, data attributes, or the Variants API for dynamic styles.
142
+ ---
129
143
 
130
- - ⚛️ **React 19+ only.** We rely on automatic ref forwarding instead of `forwardRef`. This keeps the runtime tiny but requires React 19.
144
+ ## What We Don't Do
131
145
 
132
- - **Vite only.** The plugin uses Vite's built-in AST parser and virtual module system. No Webpack, Rollup, or other bundler support.
146
+ - 🚫 **No runtime interpolation** Can't write `${props => props.color}`. Use variants, CSS variables, or data attributes.
147
+ - ⚛️ **React 19+ only** — Uses automatic ref forwarding (no `forwardRef`).
148
+ - ⚡ **Vite only** — Uses Vite's AST parser and virtual modules. No Webpack/Rollup.
149
+ - 🚫 **No `css` prop** — Use named `css` variables with `className`.
150
+ - 🚫 **No `shouldForwardProp`** — Not needed. Variants auto-strip props.
133
151
 
134
- - 💡 **Why these constraints?** Each limitation removes complexity. No runtime interpolation means no runtime CSS parsing. React 19 means no forwardRef wrapper. Vite-only means one excellent integration instead of many mediocre ones.
152
+ Each constraint removes complexityno CSS parsing, no forwardRef, one great integration.
135
153
 
136
154
  ---
137
155
 
138
156
  ## Installation
139
157
 
140
158
  ```bash
141
- npm install styled-static
159
+ npm install @alex.radulescu/styled-static
142
160
  # or
143
- bun add styled-static
161
+ bun add @alex.radulescu/styled-static
144
162
  ```
145
163
 
146
164
  Configure the Vite plugin:
147
165
 
148
166
  ```ts
149
167
  // vite.config.ts
150
- import { defineConfig } from "vite";
151
168
  import react from "@vitejs/plugin-react";
152
- import { styledStatic } from "styled-static/vite";
169
+ import { styledStatic } from "@alex.radulescu/styled-static/vite";
170
+ import { defineConfig } from "vite";
153
171
 
154
172
  export default defineConfig({
155
173
  plugins: [styledStatic(), react()],
@@ -164,10 +182,10 @@ export default defineConfig({
164
182
 
165
183
  ### styled
166
184
 
167
- Create styled React components with zero runtime overhead. CSS is extracted to static files at build time.
185
+ Create styled React components:
168
186
 
169
187
  ```tsx
170
- import { styled } from "styled-static";
188
+ import { styled } from "@alex.radulescu/styled-static";
171
189
 
172
190
  const Button = styled.button`
173
191
  padding: 0.5rem 1rem;
@@ -226,7 +244,7 @@ When components are extended, classes are ordered correctly:
226
244
  Get a scoped class name for mixing with other classes:
227
245
 
228
246
  ```tsx
229
- import { css } from 'styled-static';
247
+ import { css } from '@alex.radulescu/styled-static';
230
248
 
231
249
  const activeClass = css`
232
250
  outline: 2px solid blue;
@@ -252,7 +270,7 @@ const highlightClass = css`
252
270
  Create scoped keyframe animations. The animation name is hashed to avoid conflicts between components:
253
271
 
254
272
  ```tsx
255
- import { styled, keyframes } from "styled-static";
273
+ import { keyframes, styled } from "@alex.radulescu/styled-static";
256
274
 
257
275
  const spin = keyframes`
258
276
  from { transform: rotate(0deg); }
@@ -282,42 +300,13 @@ const PulsingDot = styled.div`
282
300
  `;
283
301
  ```
284
302
 
285
- **How it works:**
286
-
287
- - At build time, keyframes CSS is extracted to a static file
288
- - The animation name is hashed (e.g., `ss-abc123`)
289
- - References in styled components are replaced with the hashed name
290
-
291
- ```css
292
- /* Generated CSS */
293
- @keyframes ss-abc123 {
294
- from {
295
- transform: rotate(0deg);
296
- }
297
- to {
298
- transform: rotate(360deg);
299
- }
300
- }
301
- .ss-xyz789 {
302
- animation: ss-abc123 1s linear infinite;
303
- }
304
- ```
303
+ Animation names are hashed at build time to avoid conflicts.
305
304
 
306
305
  ### attrs
307
306
 
308
- Set default HTML attributes on styled components using the `.attrs()` method:
307
+ Set default HTML attributes using `.attrs()`:
309
308
 
310
309
  ```tsx
311
- import { styled } from 'styled-static';
312
-
313
- // Set default type for input
314
- const PasswordInput = styled.input.attrs({ type: 'password' })`
315
- padding: 0.5rem 1rem;
316
- border: 1px solid #e5e7eb;
317
- border-radius: 4px;
318
- `;
319
-
320
- // Set multiple default attributes
321
310
  const SubmitButton = styled.button.attrs({
322
311
  type: 'submit',
323
312
  'aria-label': 'Submit form',
@@ -327,45 +316,30 @@ const SubmitButton = styled.button.attrs({
327
316
  color: white;
328
317
  `;
329
318
 
330
- // Usage - default attrs are applied, can be overridden
331
- <PasswordInput placeholder="Enter password" />
332
- // Renders: <input type="password" placeholder="Enter password" class="ss-abc123" />
333
-
334
319
  <SubmitButton>Send</SubmitButton>
335
- // Renders: <button type="submit" aria-label="Submit form" class="ss-xyz789">Send</button>
320
+ // Renders: <button type="submit" aria-label="Submit form" class="ss-xyz789">
336
321
  ```
337
322
 
338
- > **Note:** Unlike styled-components, attrs in styled-static must be static objects (no functions). For dynamic attributes, use regular props on your component.
323
+ > **Note:** attrs must be static objects (no functions). For dynamic attributes, use regular props.
339
324
 
340
325
  ### cx Utility
341
326
 
342
- Combine class names conditionally with the minimal `cx` utility (~40 bytes):
327
+ Combine class names conditionally. Intentionally flat (no nested arrays/objects) for minimal bundle size:
343
328
 
344
329
  ```tsx
345
- import { css, cx } from 'styled-static';
330
+ import { css, cx } from '@alex.radulescu/styled-static';
346
331
 
347
- const activeClass = css`
348
- color: blue;
349
- `;
350
-
351
- // Multiple class names
352
- <div className={cx('base', 'active')} />
353
- // → class="base active"
332
+ const activeClass = css`color: blue;`;
354
333
 
355
- // Conditional classes
356
- <Button className={cx('btn', isActive && activeClass)} />
357
- // → class="btn ss-abc123" (when active)
358
- // → class="btn" (when not active)
359
-
360
- // Falsy values are filtered out
361
- <div className={cx('a', null, undefined, false, 'b')} />
362
- // → class="a b"
334
+ cx('base', 'active') // "base active"
335
+ cx('btn', isActive && activeClass) // → "btn ss-abc123" or "btn"
336
+ cx('a', null, undefined, false, 'b') // → "a b"
363
337
  ```
364
338
 
365
339
  ### Global Styles
366
340
 
367
341
  ```tsx
368
- import { createGlobalStyle } from "styled-static";
342
+ import { createGlobalStyle } from "@alex.radulescu/styled-static";
369
343
 
370
344
  const GlobalStyle = createGlobalStyle`
371
345
  * {
@@ -392,7 +366,6 @@ createRoot(document.getElementById("root")!).render(
392
366
  );
393
367
  ```
394
368
 
395
- > **Note:** The component renders nothing at runtime. All CSS is extracted and injected via imports.
396
369
 
397
370
  ### Variants API
398
371
 
@@ -403,68 +376,37 @@ For type-safe variant handling, use `styledVariants` to create components with v
403
376
  #### styledVariants
404
377
 
405
378
  ```tsx
406
- import { styledVariants, css } from "styled-static";
379
+ import { css, styledVariants } from "@alex.radulescu/styled-static";
407
380
 
408
- // With css`` for syntax highlighting (recommended)
409
381
  const Button = styledVariants({
410
382
  component: "button",
411
383
  css: css`
412
384
  padding: 0.5rem 1rem;
413
- border: none;
414
- border-radius: 4px;
415
- cursor: pointer;
385
+ background: gray;
386
+ color: white;
387
+ font-size: 1rem;
416
388
  `,
417
389
  variants: {
418
390
  color: {
419
- primary: css`
420
- background: blue;
421
- color: white;
422
- `,
423
- danger: css`
424
- background: red;
425
- color: white;
426
- `,
427
- success: css`
428
- background: green;
429
- color: white;
430
- `,
391
+ primary: css`background: blue;`,
392
+ danger: css`background: red;`,
393
+ success: css`background: green;`,
431
394
  },
432
395
  size: {
433
- sm: css`
434
- font-size: 0.875rem;
435
- padding: 0.25rem 0.5rem;
436
- `,
437
- md: css`
438
- font-size: 1rem;
439
- `,
440
- lg: css`
441
- font-size: 1.125rem;
442
- padding: 0.75rem 1.5rem;
443
- `,
396
+ sm: css`font-size: 0.875rem; padding: 0.25rem 0.5rem;`,
397
+ lg: css`font-size: 1.125rem; padding: 0.75rem 1.5rem;`,
444
398
  },
445
399
  },
446
400
  });
447
401
 
448
- // Plain strings also work (no highlighting)
449
- const SimpleButton = styledVariants({
450
- component: "button",
451
- css: `padding: 0.5rem;`,
452
- variants: {
453
- size: { sm: `font-size: 0.875rem;` },
454
- },
455
- });
456
-
457
- // Usage - variant props are type-safe
458
- <Button color="primary" size="lg">
459
- Click me
460
- </Button>;
402
+ <Button color="primary" size="lg">Click me</Button>
461
403
  // Renders: <button class="ss-abc ss-abc--color-primary ss-abc--size-lg">
462
404
  ```
463
405
 
464
406
  #### cssVariants
465
407
 
466
408
  ```tsx
467
- import { cssVariants, css, cx } from 'styled-static';
409
+ import { cssVariants, css, cx } from '@alex.radulescu/styled-static';
468
410
 
469
411
  // With css`` for syntax highlighting (recommended)
470
412
  const badgeCss = cssVariants({
@@ -496,44 +438,58 @@ const badgeCss = cssVariants({
496
438
 
497
439
  ## Features
498
440
 
499
- ### Polymorphic `as` Prop
441
+ ### Polymorphism with withComponent
500
442
 
501
- Render a styled component as a different HTML element:
443
+ Render one component with another's styles using `withComponent`:
502
444
 
503
445
  ```tsx
446
+ import { Link } from "react-router-dom";
447
+ import { styled, withComponent } from "@alex.radulescu/styled-static";
448
+
504
449
  const Button = styled.button`
505
450
  padding: 0.5rem 1rem;
506
451
  background: blue;
507
452
  color: white;
508
453
  `;
509
454
 
510
- // Render as an anchor tag
511
- <Button as="a" href="/link">
512
- I'm a link styled as a button
513
- </Button>;
455
+ // Create a Link that looks like Button
456
+ const LinkButton = withComponent(Link, Button);
457
+
458
+ // Also works with HTML tags
459
+ const AnchorButton = withComponent('a', Button);
460
+
461
+ // Usage
462
+ <LinkButton to="/path">Router link styled as button</LinkButton>
463
+ <AnchorButton href="/external">External link</AnchorButton>
514
464
  ```
515
465
 
516
- ### Transient Props
466
+ `withComponent` accepts:
467
+
468
+ - **First argument**: The component to render (React component or HTML tag string)
469
+ - **Second argument**: The styled component whose styles to use
470
+
471
+ ### Manual Composition with .className
517
472
 
518
- Props prefixed with `$` are filtered out and won't reach the DOM. This is useful for passing data through components without polluting the HTML:
473
+ Every styled component exposes a static `.className` property for manual composition:
519
474
 
520
475
  ```tsx
521
- // $-prefixed props are filtered from the DOM
522
- <Button $trackingId="hero-cta" onClick={handleClick}>
523
- Click
524
- </Button>;
525
- // Renders: <button class="ss-abc123">Click</button>
526
- // The $trackingId prop is available in event handlers but not in HTML
527
-
528
- // Useful for component composition
529
- const Card = styled.div`...`;
530
- <Card $featured={true} $size="large" className="my-card">
531
- Content
532
- </Card>;
533
- // Renders: <div class="ss-abc123 my-card">Content</div>
476
+ const Button = styled.button`
477
+ padding: 0.5rem 1rem;
478
+ background: blue;
479
+ `;
480
+
481
+ // Use className directly on any element
482
+ <a className={Button.className} href="/link">
483
+ Link with button styles
484
+ </a>
485
+
486
+ // Combine with cx utility
487
+ <div className={cx(Button.className, Card.className, "custom")}>
488
+ Combined styles
489
+ </div>
534
490
  ```
535
491
 
536
- > **Note:** Since styled-static extracts CSS at build time, transient props cannot be used for dynamic styling. Use class toggling, data attributes, or CSS variables instead.
492
+ This is useful when you need button styles on a non-component element or want to combine multiple styled component classes.
537
493
 
538
494
  ### CSS Nesting
539
495
 
@@ -574,281 +530,175 @@ const Card = styled.div`
574
530
 
575
531
  ## Dynamic Styling
576
532
 
577
- Since CSS is extracted at build time, you cannot use runtime interpolations like `${props => props.color}`. Instead, use these patterns:
578
-
579
- ### 1. Variants API (Recommended)
580
-
581
- For type-safe variant handling, use `styledVariants` or `cssVariants`:
582
-
583
- ```tsx
584
- import { styledVariants, css } from "styled-static";
585
-
586
- const Button = styledVariants({
587
- component: "button",
588
- css: css`
589
- padding: 0.5rem 1rem;
590
- border: none;
591
- border-radius: 4px;
592
- `,
593
- variants: {
594
- color: {
595
- primary: css`background: blue; color: white;`,
596
- danger: css`background: red; color: white;`,
597
- success: css`background: green; color: white;`,
598
- },
599
- },
600
- });
533
+ No runtime interpolation—use these patterns instead:
534
+ - **[Variants API](#variants-api)** — Type-safe component variants (recommended)
535
+ - **[cx utility](#cx-utility)** Conditional class toggling
536
+ - **CSS variables** — Pass via `style` prop for truly dynamic values
537
+ - **Data attributes** Style with `&[data-variant="x"]` selectors
601
538
 
602
- // Usage - variant props are type-safe
603
- <Button color="primary">Click</Button>
604
- <Button color="danger">Delete</Button>
605
- ```
539
+ ---
606
540
 
607
- See the [Variants API](#variants-api) section for full documentation.
541
+ ## Theming
608
542
 
609
- ### 2. Class Toggling with cx
543
+ CSS-first theming with CSS variables and `data-theme` attributes:
610
544
 
611
545
  ```tsx
612
- import { styled, css, cx } from "styled-static";
613
-
614
- const primaryClass = css`
615
- background: blue;
616
- `;
617
- const dangerClass = css`
618
- background: red;
546
+ const GlobalStyle = createGlobalStyle`
547
+ :root, [data-theme="light"] { --bg: #fff; --text: #1a1a1a; }
548
+ [data-theme="dark"] { --bg: #0a0a0a; --text: #f1f5f9; }
549
+ [data-theme="pokemon"] { --bg: #ffcb05; --text: #2a75bb; }
619
550
  `;
620
551
 
621
- const Button = styled.button`
622
- padding: 0.5rem 1rem;
623
- color: white;
552
+ const Card = styled.div`
553
+ background: var(--bg);
554
+ color: var(--text);
624
555
  `;
625
-
626
- // Toggle between classes based on props/state
627
- <Button className={cx(isPrimary ? primaryClass : dangerClass)}>Click</Button>;
628
556
  ```
629
557
 
630
- ### 3. Data Attributes
558
+ ### Theme Helpers
631
559
 
632
560
  ```tsx
633
- const Button = styled.button`
634
- padding: 0.5rem 1rem;
635
- color: white;
636
-
637
- &[data-variant="primary"] {
638
- background: blue;
639
- }
640
- &[data-variant="danger"] {
641
- background: red;
642
- }
643
- &[data-variant="success"] {
644
- background: green;
645
- }
646
- `;
561
+ import { initTheme, setTheme, getTheme, onSystemThemeChange } from "@alex.radulescu/styled-static";
647
562
 
648
- <Button data-variant={variant}>Click</Button>;
649
- ```
563
+ // Initialize (reads localStorage → system preference → default)
564
+ initTheme({ defaultTheme: "light", useSystemPreference: true });
650
565
 
651
- ### 4. CSS Variables
566
+ // Switch themes
567
+ setTheme("dark"); // persists to localStorage
568
+ setTheme("pokemon", false); // no persist (preview)
652
569
 
653
- ```tsx
654
- const Button = styled.button`
655
- padding: 0.5rem 1rem;
656
- background: var(--btn-bg, gray);
657
- color: var(--btn-color, white);
658
- `;
570
+ // Read current
571
+ const current = getTheme(); // 'light' | 'dark' | etc.
659
572
 
660
- <Button style={{ "--btn-bg": color, "--btn-color": textColor }}>Click</Button>;
573
+ // React to OS changes
574
+ const unsub = onSystemThemeChange((prefersDark) => {
575
+ if (!localStorage.getItem("theme")) setTheme(prefersDark ? "dark" : "light", false);
576
+ });
661
577
  ```
662
578
 
663
- ---
664
-
665
- ## Theming
666
-
667
- styled-static provides a simple, CSS-first approach to theming using CSS variables and `data-theme` attributes. No runtime overhead—just pure CSS.
668
-
669
- ### Defining Themes
579
+ | Function | Description |
580
+ | -------- | ----------- |
581
+ | `initTheme(options?)` | Init on load. Priority: localStorage → system → default |
582
+ | `setTheme(theme, persist?)` | Set theme. Persists to localStorage by default |
583
+ | `getTheme()` | Get current theme from `data-theme` |
584
+ | `onSystemThemeChange(cb)` | Subscribe to OS theme changes |
670
585
 
671
- Use `createGlobalStyle` to define your theme tokens:
586
+ ---
672
587
 
673
- ```tsx
674
- import { createGlobalStyle, styled } from "styled-static";
588
+ ## Troubleshooting
675
589
 
676
- const GlobalStyle = createGlobalStyle`
677
- :root, [data-theme="light"] {
678
- --color-bg: #ffffff;
679
- --color-text: #1a1a1a;
680
- --color-primary: #3b82f6;
681
- --color-accent: #8b5cf6;
682
- }
683
-
684
- [data-theme="dark"] {
685
- --color-bg: #0f172a;
686
- --color-text: #f1f5f9;
687
- --color-primary: #60a5fa;
688
- --color-accent: #a78bfa;
689
- }
690
-
691
- /* Custom themes */
692
- [data-theme="pokemon"] {
693
- --color-bg: #ffcb05;
694
- --color-text: #2a75bb;
695
- --color-primary: #cc0000;
696
- --color-accent: #3d7dca;
697
- }
698
-
699
- [data-theme="star-trek"] {
700
- --color-bg: #000000;
701
- --color-text: #ff9900;
702
- --color-primary: #3366cc;
703
- --color-accent: #cc0000;
704
- }
705
- `;
590
+ ### Storybook: "This package is ESM only"
706
591
 
707
- // Use CSS variables in your components
708
- const Button = styled.button`
709
- background: var(--color-primary);
710
- color: var(--color-bg);
711
- padding: 0.75rem 1.5rem;
712
- border: none;
713
- border-radius: 4px;
592
+ If you see this error when using styled-static with Storybook:
714
593
 
715
- &:hover {
716
- background: var(--color-accent);
717
- }
718
- `;
594
+ ```
595
+ Failed to resolve "@alex.radulescu/styled-static/vite".
596
+ This package is ESM only but it was tried to load by `require`.
719
597
  ```
720
598
 
721
- ### Theme Helper Functions
599
+ Add the package to Vite's `optimizeDeps.include` in your Storybook config:
722
600
 
723
- styled-static provides helper functions for theme switching:
601
+ ```ts
602
+ // .storybook/main.ts
603
+ export default {
604
+ // ... other config
605
+ viteFinal: async (config) => {
606
+ config.optimizeDeps = config.optimizeDeps || {};
607
+ config.optimizeDeps.include = [
608
+ ...(config.optimizeDeps.include || []),
609
+ '@alex.radulescu/styled-static',
610
+ ];
611
+ return config;
612
+ },
613
+ };
614
+ ```
724
615
 
725
- ```tsx
726
- import {
727
- initTheme,
728
- setTheme,
729
- getTheme,
730
- onSystemThemeChange,
731
- } from "styled-static";
732
-
733
- // Initialize on app load (reads from localStorage, falls back to default)
734
- initTheme({
735
- defaultTheme: "light",
736
- useSystemPreference: true, // Optional: detect OS dark mode
737
- });
616
+ This is a known limitation with ESM-only packages in Storybook's esbuild-based config loading.
738
617
 
739
- // Get current theme
740
- const current = getTheme(); // 'light' | 'dark' | 'pokemon' | etc.
618
+ ---
741
619
 
742
- // Change theme (persists to localStorage by default)
743
- setTheme("dark");
620
+ ## How It Works
744
621
 
745
- // Change without persisting (useful for previews)
746
- setTheme("pokemon", false);
622
+ styled-static uses a Vite plugin to transform your styled components at build time. Here's what happens under the hood:
747
623
 
748
- // Listen for OS theme changes
749
- const unsubscribe = onSystemThemeChange((prefersDark) => {
750
- if (!localStorage.getItem("theme")) {
751
- setTheme(prefersDark ? "dark" : "light", false);
752
- }
753
- });
754
- ```
624
+ ### Build-Time Transformation
755
625
 
756
- ### Theme Toggle Example
626
+ When you write a styled component, the Vite plugin intercepts your code and performs AST-based transformation:
757
627
 
758
628
  ```tsx
759
- import { getTheme, setTheme } from "styled-static";
629
+ // 1. What you write:
630
+ import { styled } from "@alex.radulescu/styled-static";
760
631
 
761
- function ThemeToggle() {
762
- const toggleTheme = () => {
763
- const current = getTheme();
764
- setTheme(current === "dark" ? "light" : "dark");
765
- };
632
+ const Button = styled.button`
633
+ padding: 1rem;
634
+ background: blue;
635
+ color: white;
636
+ `;
766
637
 
767
- return <button onClick={toggleTheme}>Toggle Theme</button>;
768
- }
638
+ // 2. What gets generated:
639
+ import { createElement } from "react";
640
+ import { m } from "@alex.radulescu/styled-static/runtime";
641
+ import "@alex.radulescu/styled-static:abc123-0.css";
769
642
 
770
- function ThemeSelector() {
771
- return (
772
- <select onChange={(e) => setTheme(e.target.value)}>
773
- <option value="light">Light</option>
774
- <option value="dark">Dark</option>
775
- <option value="pokemon">Pokemon</option>
776
- <option value="star-trek">Star Trek</option>
777
- </select>
778
- );
779
- }
643
+ const Button = Object.assign(
644
+ (p) => createElement("button", {...p, className: m("ss-abc123", p.className)}),
645
+ { className: "ss-abc123" }
646
+ );
780
647
  ```
781
648
 
782
- ### System Preference Detection
649
+ 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.
783
650
 
784
- Combine `useSystemPreference` with CSS for automatic system theme detection:
651
+ ### Virtual CSS Modules
785
652
 
786
- ```tsx
787
- const GlobalStyle = createGlobalStyle`
788
- :root {
789
- --color-bg: #ffffff;
790
- --color-text: #1a1a1a;
791
- }
792
-
793
- /* Automatic system preference (when no explicit theme set) */
794
- @media (prefers-color-scheme: dark) {
795
- :root:not([data-theme]) {
796
- --color-bg: #0f172a;
797
- --color-text: #f1f5f9;
798
- }
799
- }
800
-
801
- /* Explicit theme overrides */
802
- [data-theme="light"] { --color-bg: #ffffff; --color-text: #1a1a1a; }
803
- [data-theme="dark"] { --color-bg: #0f172a; --color-text: #f1f5f9; }
804
- `;
653
+ Each styled component gets its own virtual CSS module with a unique ID like `styled-static:abc123-0.css`. This approach enables:
805
654
 
806
- // Initialize with system preference detection
807
- initTheme({ useSystemPreference: true });
655
+ - **Deduplication** - CSS is optimized by Vite's pipeline
656
+ - **Code splitting** - CSS loads only with the components that use it
657
+ - ✅ **Hot Module Replacement** - Changes to styles trigger instant HMR
658
+ - ✅ **Production optimization** - CSS can be extracted to a single file
659
+
660
+ ```css
661
+ /* Virtual module: styled-static:abc123-0.css */
662
+ .ss-abc123 {
663
+ padding: 1rem;
664
+ background: blue;
665
+ color: white;
666
+ }
808
667
  ```
809
668
 
810
- ### API Reference
669
+ ### Minimal Runtime
811
670
 
812
- | Function | Description |
813
- | ------------------------------------- | ------------------------------------------------------------------------------------ |
814
- | `getTheme(attribute?)` | Get current theme from `data-theme` attribute. Returns `'light'` as default. |
815
- | `setTheme(theme, persist?, options?)` | Set theme on document. Persists to localStorage by default. |
816
- | `initTheme(options?)` | Initialize theme on page load. Priority: localStorage → system preference → default. |
817
- | `onSystemThemeChange(callback)` | Subscribe to OS theme changes. Returns unsubscribe function. |
671
+ The runtime is extremely small because components are generated inline at build time. The only runtime code is a className merge helper:
818
672
 
819
- #### initTheme Options
673
+ | Module | Minified | Brotli |
674
+ | ---------------- | -------- | ------ |
675
+ | runtime/index.js | **45 B** | 50 B |
820
676
 
821
- ```ts
822
- initTheme({
823
- defaultTheme: "light", // Default theme if no stored preference
824
- storageKey: "theme", // localStorage key (default: 'theme')
825
- useSystemPreference: false, // Detect OS dark/light preference
826
- attribute: "data-theme", // Attribute to set on documentElement
827
- });
677
+ This is a **98% reduction** from traditional CSS-in-JS libraries.
678
+
679
+ ```tsx
680
+ // The ENTIRE runtime - just className merging
681
+ export const m = (base, user) => user ? `${base} ${user}` : base;
828
682
  ```
829
683
 
830
- ---
684
+ Everything else is generated at build time as inline components.
831
685
 
832
- ## How It Works
686
+ ### Zero-Runtime Features
833
687
 
834
- styled-static transforms your code at build time:
688
+ Some features have literally zero runtime cost because they're completely replaced at build time:
835
689
 
836
690
  ```tsx
837
- // You write:
838
- const Button = styled.button`
839
- padding: 1rem;
840
- background: blue;
841
- `;
691
+ // css helper - zero runtime (just a string)
692
+ const activeClass = css`outline: 2px solid blue;`;
693
+ // Generated: const activeClass = "ss-xyz789";
842
694
 
843
- // Becomes:
844
- import "styled-static:abc123-0.css";
845
- const Button = __styled("button", "ss-def456", "Button");
695
+ // Global styles - zero runtime (just CSS import)
696
+ const GlobalStyles = createGlobalStyle`* { box-sizing: border-box; }`;
697
+ // Generated: const GlobalStyles = () => null;
846
698
 
847
- // CSS extracted to static file:
848
- .ss-def456 {
849
- padding: 1rem;
850
- background: blue;
851
- }
699
+ // withComponent - zero runtime (build-time transformation)
700
+ const LinkButton = withComponent(Link, Button);
701
+ // Generated: Object.assign((p) => createElement(Link, {...p, className: m(Button.className, p.className)}), { className: Button.className })
852
702
  ```
853
703
 
854
704
  ---
@@ -874,67 +724,53 @@ const Button = styled.button`...`;
874
724
  // ✅ Type-safe: button props are available
875
725
  <Button type="submit" disabled>Submit</Button>
876
726
 
877
- // ✅ Type-safe: as prop changes available props
878
- <Button as="a" href="/link">Link</Button>
727
+ // ✅ Type-safe: withComponent infers props from target component
728
+ const LinkButton = withComponent(Link, Button);
729
+ <LinkButton to="/path">Link</LinkButton>
879
730
 
880
- // ✅ Transient props are typed
881
- <Button $primary={true}>Primary</Button>
731
+ // ✅ Type-safe: .className is always string
732
+ const classes = Button.className; // string
882
733
  ```
883
734
 
884
735
  ---
885
736
 
886
- ## Limitations
887
-
888
- - **No runtime interpolation** - CSS values must be static (use CSS variables for dynamic values)
889
- - **React 19+ only** - Uses automatic ref forwarding
890
- - **Vite only** - Built specifically for Vite's plugin system
891
-
892
- ---
893
-
894
737
  ## Zero Dependencies
895
738
 
896
- The plugin has **ZERO** direct dependencies! 🎉
897
-
898
- It relies on:
739
+ Zero runtime dependencies. Uses native CSS nesting (Chrome 112+, Safari 16.5+, Firefox 117+) and Vite's CSS pipeline. See [Installation](#installation) for optional Lightning CSS integration.
899
740
 
900
- - **Native CSS nesting** - Supported in all browsers that support React 19 (Chrome 112+, Safari 16.5+, Firefox 117+, Edge 112+)
901
- - **Vite's CSS pipeline** - Handles the virtual CSS modules
902
-
903
- ### Optional: Lightning CSS
741
+ ---
904
742
 
905
- For faster CSS processing and automatic vendor prefixes, install Lightning CSS:
743
+ ## Comparison
906
744
 
907
- ```bash
908
- npm install lightningcss
909
- ```
745
+ **Legend:** ✓ Yes | ◐ Partial | ✗ No
910
746
 
911
- Then enable in your vite.config.ts:
747
+ | | styled-static | Emotion | Linaria | [Restyle](https://restyle.dev) | Panda CSS |
748
+ |-|---------------|---------|---------|--------|-----------|
749
+ | Runtime | **~50 B** | ~11 KB | ~1.5 KB | ~2.2 KB | 0 B |
750
+ | Dependencies | 0 | 5+ | 10+ | 0 | 5+ |
751
+ | React | 19+ | 16+ | 16+ | 19+ | 16+ |
752
+ | Bundler | Vite | Any | Many | Any | Any |
753
+ | `styled.el` | ✓ | ✓ | ✓ | ✓ | ◐ |
754
+ | `styled(Comp)` | ✓ | ✓ | ✓ | ✓ | ◐ |
755
+ | Variants | ✓ | ◐ | ◐ | ◐ | ✓ |
756
+ | `css` helper | ✓ | ✓ | ✓ | ✓ | ✓ |
757
+ | `css` inline prop | ✗ | ✓ | ✗ | ✓ | ✓ |
758
+ | Runtime interpolation | ✗ | ✓ | ✗ | ✓ | ✗ |
759
+ | `.className` access | ✓ | ✗ | ✗ | ✗ | ✗ |
912
760
 
913
- ```ts
914
- export default defineConfig({
915
- css: { transformer: "lightningcss" },
916
- plugins: [styledStatic(), react()],
917
- });
918
- ```
761
+ **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](https://restyle.dev) for `css` prop + Server Components. Panda for atomic CSS + design tokens.
919
762
 
920
763
  ---
921
764
 
922
- ## Comparison
765
+ ## VS Code Support
923
766
 
924
- | Feature | styled-static | styled-components | Emotion | Linaria |
925
- | --------------------- | ------------- | ----------------- | ------- | ------- |
926
- | Zero Runtime | ✅ | ❌ | ❌ | ✅ |
927
- | Runtime Interpolation | ❌ | ✅ | ✅ | ❌ |
928
- | `as` prop | ✅ | ✅ | ✅ | ❌ |
929
- | Component Extension | ✅ | ✅ | ✅ | ✅ |
930
- | Bundle Size | ~300B | ~12KB | ~11KB | ~0B |
931
- | Direct Dependencies | 0 | 7 | 5 | 10+ |
767
+ For syntax highlighting in template literals, install the [vscode-styled-components](https://marketplace.visualstudio.com/items?itemName=styled-components.vscode-styled-components) extension.
932
768
 
933
769
  ---
934
770
 
935
- ## VS Code Support
771
+ ## Inspiration
936
772
 
937
- For syntax highlighting in template literals, install the [vscode-styled-components](https://marketplace.visualstudio.com/items?itemName=styled-components.vscode-styled-components) extension.
773
+ We take inspiration from the greats before us: [Emotion](https://emotion.sh), [styled-components](https://styled-components.com), [Linaria](https://linaria.dev), [Panda CSS](https://panda-css.com), [Pigment CSS](https://github.com/mui/pigment-css), [Stitches](https://stitches.dev), [Ecsstatic](https://github.com/danielroe/ecsstatic), [Restyle](https://restyle.dev), [goober](https://goober.rocks). Thanks to each and every one for ideas and inspiration.
938
774
 
939
775
  ---
940
776