@alex.radulescu/styled-static 0.2.0 → 0.7.1

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,37 +104,52 @@ 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:** [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
 
@@ -147,9 +165,9 @@ 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
169
  import { styledStatic } from "styled-static/vite";
170
+ import { defineConfig } from "vite";
153
171
 
154
172
  export default defineConfig({
155
173
  plugins: [styledStatic(), react()],
@@ -164,7 +182,7 @@ 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
188
  import { styled } from "styled-static";
@@ -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 "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,39 +316,24 @@ 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
330
  import { css, cx } from '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
@@ -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,61 +376,30 @@ 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 "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
 
@@ -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 "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,143 @@ 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`:
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
582
538
 
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
- });
601
-
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;
561
+ import { initTheme, setTheme, getTheme, onSystemThemeChange } from "styled-static";
636
562
 
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
- `;
647
-
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
 
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 |
585
+
663
586
  ---
664
587
 
665
- ## Theming
588
+ ## How It Works
666
589
 
667
- styled-static provides a simple, CSS-first approach to theming using CSS variables and `data-theme` attributes. No runtime overhead—just pure CSS.
590
+ styled-static uses a Vite plugin to transform your styled components at build time. Here's what happens under the hood:
668
591
 
669
- ### Defining Themes
592
+ ### Build-Time Transformation
670
593
 
671
- Use `createGlobalStyle` to define your theme tokens:
594
+ When you write a styled component, the Vite plugin intercepts your code and performs AST-based transformation:
672
595
 
673
596
  ```tsx
674
- import { createGlobalStyle, styled } from "styled-static";
675
-
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
- `;
597
+ // 1. What you write:
598
+ import { styled } from "styled-static";
706
599
 
707
- // Use CSS variables in your components
708
600
  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;
714
-
715
- &:hover {
716
- background: var(--color-accent);
717
- }
601
+ padding: 1rem;
602
+ background: blue;
603
+ color: white;
718
604
  `;
719
- ```
720
605
 
721
- ### Theme Helper Functions
722
-
723
- styled-static provides helper functions for theme switching:
724
-
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
- });
738
-
739
- // Get current theme
740
- const current = getTheme(); // 'light' | 'dark' | 'pokemon' | etc.
741
-
742
- // Change theme (persists to localStorage by default)
743
- setTheme("dark");
744
-
745
- // Change without persisting (useful for previews)
746
- setTheme("pokemon", false);
606
+ // 2. What gets generated:
607
+ import { createElement } from "react";
608
+ import { m } from "styled-static/runtime";
609
+ import "styled-static:abc123-0.css";
747
610
 
748
- // Listen for OS theme changes
749
- const unsubscribe = onSystemThemeChange((prefersDark) => {
750
- if (!localStorage.getItem("theme")) {
751
- setTheme(prefersDark ? "dark" : "light", false);
752
- }
753
- });
611
+ const Button = Object.assign(
612
+ (p) => createElement("button", {...p, className: m("ss-abc123", p.className)}),
613
+ { className: "ss-abc123" }
614
+ );
754
615
  ```
755
616
 
756
- ### Theme Toggle Example
617
+ 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.
757
618
 
758
- ```tsx
759
- import { getTheme, setTheme } from "styled-static";
619
+ ### Virtual CSS Modules
760
620
 
761
- function ThemeToggle() {
762
- const toggleTheme = () => {
763
- const current = getTheme();
764
- setTheme(current === "dark" ? "light" : "dark");
765
- };
621
+ Each styled component gets its own virtual CSS module with a unique ID like `styled-static:abc123-0.css`. This approach enables:
766
622
 
767
- return <button onClick={toggleTheme}>Toggle Theme</button>;
768
- }
623
+ - **Deduplication** - CSS is optimized by Vite's pipeline
624
+ - ✅ **Code splitting** - CSS loads only with the components that use it
625
+ - ✅ **Hot Module Replacement** - Changes to styles trigger instant HMR
626
+ - ✅ **Production optimization** - CSS can be extracted to a single file
769
627
 
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
- );
628
+ ```css
629
+ /* Virtual module: styled-static:abc123-0.css */
630
+ .ss-abc123 {
631
+ padding: 1rem;
632
+ background: blue;
633
+ color: white;
779
634
  }
780
635
  ```
781
636
 
782
- ### System Preference Detection
783
-
784
- Combine `useSystemPreference` with CSS for automatic system theme detection:
785
-
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
- `;
805
-
806
- // Initialize with system preference detection
807
- initTheme({ useSystemPreference: true });
808
- ```
637
+ ### Minimal Runtime
809
638
 
810
- ### API Reference
639
+ The runtime is extremely small because components are generated inline at build time. The only runtime code is a className merge helper:
811
640
 
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. |
641
+ | Module | Minified | Brotli |
642
+ | ---------------- | -------- | ------ |
643
+ | runtime/index.js | **45 B** | 50 B |
818
644
 
819
- #### initTheme Options
645
+ This is a **98% reduction** from traditional CSS-in-JS libraries.
820
646
 
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
- });
647
+ ```tsx
648
+ // The ENTIRE runtime - just className merging
649
+ export const m = (base, user) => user ? `${base} ${user}` : base;
828
650
  ```
829
651
 
830
- ---
652
+ Everything else is generated at build time as inline components.
831
653
 
832
- ## How It Works
654
+ ### Zero-Runtime Features
833
655
 
834
- styled-static transforms your code at build time:
656
+ Some features have literally zero runtime cost because they're completely replaced at build time:
835
657
 
836
658
  ```tsx
837
- // You write:
838
- const Button = styled.button`
839
- padding: 1rem;
840
- background: blue;
841
- `;
659
+ // css helper - zero runtime (just a string)
660
+ const activeClass = css`outline: 2px solid blue;`;
661
+ // Generated: const activeClass = "ss-xyz789";
842
662
 
843
- // Becomes:
844
- import "styled-static:abc123-0.css";
845
- const Button = __styled("button", "ss-def456", "Button");
663
+ // Global styles - zero runtime (just CSS import)
664
+ const GlobalStyles = createGlobalStyle`* { box-sizing: border-box; }`;
665
+ // Generated: const GlobalStyles = () => null;
846
666
 
847
- // CSS extracted to static file:
848
- .ss-def456 {
849
- padding: 1rem;
850
- background: blue;
851
- }
667
+ // withComponent - zero runtime (build-time transformation)
668
+ const LinkButton = withComponent(Link, Button);
669
+ // Generated: Object.assign((p) => createElement(Link, {...p, className: m(Button.className, p.className)}), { className: Button.className })
852
670
  ```
853
671
 
854
672
  ---
@@ -874,67 +692,53 @@ const Button = styled.button`...`;
874
692
  // ✅ Type-safe: button props are available
875
693
  <Button type="submit" disabled>Submit</Button>
876
694
 
877
- // ✅ Type-safe: as prop changes available props
878
- <Button as="a" href="/link">Link</Button>
695
+ // ✅ Type-safe: withComponent infers props from target component
696
+ const LinkButton = withComponent(Link, Button);
697
+ <LinkButton to="/path">Link</LinkButton>
879
698
 
880
- // ✅ Transient props are typed
881
- <Button $primary={true}>Primary</Button>
699
+ // ✅ Type-safe: .className is always string
700
+ const classes = Button.className; // string
882
701
  ```
883
702
 
884
703
  ---
885
704
 
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
705
  ## Zero Dependencies
895
706
 
896
- The plugin has **ZERO** direct dependencies! 🎉
897
-
898
- It relies on:
707
+ 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
708
 
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
709
+ ---
902
710
 
903
- ### Optional: Lightning CSS
711
+ ## Comparison
904
712
 
905
- For faster CSS processing and automatic vendor prefixes, install Lightning CSS:
713
+ **Legend:** Yes | Partial | No
906
714
 
907
- ```bash
908
- npm install lightningcss
909
- ```
715
+ | | styled-static | Emotion | Linaria | [Restyle](https://restyle.dev) | Panda CSS |
716
+ |-|---------------|---------|---------|--------|-----------|
717
+ | Runtime | **~50 B** | ~11 KB | ~1.5 KB | ~2.2 KB | 0 B |
718
+ | Dependencies | 0 | 5+ | 10+ | 0 | 5+ |
719
+ | React | 19+ | 16+ | 16+ | 19+ | 16+ |
720
+ | Bundler | Vite | Any | Many | Any | Any |
721
+ | `styled.el` | ✓ | ✓ | ✓ | ✓ | ◐ |
722
+ | `styled(Comp)` | ✓ | ✓ | ✓ | ✓ | ◐ |
723
+ | Variants | ✓ | ◐ | ◐ | ◐ | ✓ |
724
+ | `css` helper | ✓ | ✓ | ✓ | ✓ | ✓ |
725
+ | `css` inline prop | ✗ | ✓ | ✗ | ✓ | ✓ |
726
+ | Runtime interpolation | ✗ | ✓ | ✗ | ✓ | ✗ |
727
+ | `.className` access | ✓ | ✗ | ✗ | ✗ | ✗ |
910
728
 
911
- Then enable in your vite.config.ts:
912
-
913
- ```ts
914
- export default defineConfig({
915
- css: { transformer: "lightningcss" },
916
- plugins: [styledStatic(), react()],
917
- });
918
- ```
729
+ **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
730
 
920
731
  ---
921
732
 
922
- ## Comparison
733
+ ## VS Code Support
923
734
 
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+ |
735
+ For syntax highlighting in template literals, install the [vscode-styled-components](https://marketplace.visualstudio.com/items?itemName=styled-components.vscode-styled-components) extension.
932
736
 
933
737
  ---
934
738
 
935
- ## VS Code Support
739
+ ## Inspiration
936
740
 
937
- For syntax highlighting in template literals, install the [vscode-styled-components](https://marketplace.visualstudio.com/items?itemName=styled-components.vscode-styled-components) extension.
741
+ 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
742
 
939
743
  ---
940
744