@alex.radulescu/styled-static 0.2.0

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 ADDED
@@ -0,0 +1,943 @@
1
+ <!--
2
+ ═══════════════════════════════════════════════════════════════════════════════
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.
6
+
7
+ Key APIs: styled, css, createGlobalStyle, styledVariants, cssVariants, cx
8
+ Theme helpers: initTheme, setTheme, getTheme, onSystemThemeChange
9
+ Runtime: ~300 bytes | Dependencies: 0 | React 19+ required | Vite only
10
+
11
+ For implementation details, see CLAUDE.md or the source files in src/
12
+ ═══════════════════════════════════════════════════════════════════════════════
13
+ -->
14
+
15
+ # styled-static
16
+
17
+ Zero-runtime CSS-in-JS for React 19+ with Vite. Write styled-components syntax, get static CSS extracted at build time.
18
+
19
+ ## Features
20
+
21
+ - ⚡ **Zero Runtime** - CSS extracted at build time, no CSS-in-JS overhead
22
+ - 🎯 **Type-Safe** - Full TypeScript support with proper prop inference
23
+ - 🎨 **Familiar API** - styled-components syntax you already know
24
+ - 📦 **Tiny** - ~300 bytes runtime for `as` prop and transient props support
25
+ - 🔧 **Zero Dependencies** - Uses native CSS features and Vite's built-in tools
26
+ - 🌓 **Theme Helpers** - Simple utilities for dark mode and custom themes
27
+
28
+ ---
29
+
30
+ ## Quick Overview
31
+
32
+ All the APIs you need at a glance. styled-static provides 6 core functions that cover most CSS-in-JS use cases:
33
+
34
+ ### styled.element
35
+
36
+ Style HTML elements with template literals:
37
+
38
+ ```tsx
39
+ const Button = styled.button`
40
+ padding: 0.5rem 1rem;
41
+ ...
42
+ `;
43
+
44
+ const PrimaryButton = styled(Button)`
45
+ font-weight: bold;
46
+ ...
47
+ `;
48
+
49
+ const activeClass = css`
50
+ outline: 2px solid blue;
51
+ ...
52
+ `;
53
+
54
+ <Button className={isActive ? activeClass : ""}>Click</Button>;
55
+
56
+ const GlobalStyle = createGlobalStyle`
57
+ * { box-sizing: border-box; }
58
+ body { margin: 0; font-family: system-ui; }
59
+ `;
60
+
61
+ <GlobalStyle />; // Render once at app root
62
+
63
+ // With css`` for IDE syntax highlighting (recommended)
64
+ const Button = styledVariants({
65
+ component: "button",
66
+ css: css`
67
+ padding: 0.5rem 1rem;
68
+ border-radius: 4px;
69
+ `,
70
+ variants: {
71
+ size: {
72
+ sm: css`
73
+ font-size: 0.875rem;
74
+ `,
75
+ lg: css`
76
+ font-size: 1.125rem;
77
+ `,
78
+ },
79
+ },
80
+ });
81
+
82
+ <Button size="lg">Large Button</Button>;
83
+
84
+ const badgeCss = cssVariants({
85
+ css: css`
86
+ padding: 0.25rem 0.5rem;
87
+ border-radius: 4px;
88
+ `,
89
+ variants: {
90
+ color: {
91
+ blue: css`
92
+ background: #e0f2fe;
93
+ color: #0369a1;
94
+ `,
95
+ green: css`
96
+ background: #dcfce7;
97
+ color: #166534;
98
+ `,
99
+ },
100
+ },
101
+ });
102
+
103
+ <span className={badgeCss({ color: "blue" })}>Info</span>;
104
+ ```
105
+
106
+ ---
107
+
108
+ ## Why styled-static?
109
+
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.
111
+
112
+ - 😵 **CSS-in-JS fatigue is real.** Most libraries are now obsolete, overly complex, or have large runtime overhead. The ecosystem needs simpler solutions.
113
+
114
+ - ✨ **Syntactic sugar over CSS modules.** Most projects don't need runtime interpolation. They need a better DX for writing and organizing CSS.
115
+
116
+ - 🔒 **Supply chain security matters.** Zero dependencies means a minimal attack surface. No transitive dependencies to audit or worry about.
117
+
118
+ - 🎯 **Intentionally simple.** 95% native browser foundation + 5% sprinkles on top. We leverage what browsers already do well.
119
+
120
+ - 🎉 **Built for fun.** Sometimes the best projects come from curiosity and the joy of building something useful.
121
+
122
+ ---
123
+
124
+ ## What We Don't Do
125
+
126
+ styled-static is intentionally limited. Here's what we don't support—and why:
127
+
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.
129
+
130
+ - ⚛️ **React 19+ only.** We rely on automatic ref forwarding instead of `forwardRef`. This keeps the runtime tiny but requires React 19.
131
+
132
+ - ⚡ **Vite only.** The plugin uses Vite's built-in AST parser and virtual module system. No Webpack, Rollup, or other bundler support.
133
+
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.
135
+
136
+ ---
137
+
138
+ ## Installation
139
+
140
+ ```bash
141
+ npm install styled-static
142
+ # or
143
+ bun add styled-static
144
+ ```
145
+
146
+ Configure the Vite plugin:
147
+
148
+ ```ts
149
+ // vite.config.ts
150
+ import { defineConfig } from "vite";
151
+ import react from "@vitejs/plugin-react";
152
+ import { styledStatic } from "styled-static/vite";
153
+
154
+ export default defineConfig({
155
+ plugins: [styledStatic(), react()],
156
+ });
157
+ ```
158
+
159
+ > **Note:** The plugin must be placed **before** the React plugin in the plugins array.
160
+
161
+ ---
162
+
163
+ ## API Reference
164
+
165
+ ### styled
166
+
167
+ Create styled React components with zero runtime overhead. CSS is extracted to static files at build time.
168
+
169
+ ```tsx
170
+ import { styled } from "styled-static";
171
+
172
+ const Button = styled.button`
173
+ padding: 0.5rem 1rem;
174
+ background: #3b82f6;
175
+ color: white;
176
+ border: none;
177
+ border-radius: 4px;
178
+ cursor: pointer;
179
+
180
+ &:hover {
181
+ background: #2563eb;
182
+ }
183
+ `;
184
+
185
+ // Usage
186
+ <Button onClick={handleClick}>Click me</Button>;
187
+ ```
188
+
189
+ ### Component Extension
190
+
191
+ Extend existing styled components by passing them to `styled()`:
192
+
193
+ ```tsx
194
+ const Button = styled.button`
195
+ padding: 0.5rem 1rem;
196
+ border-radius: 4px;
197
+ `;
198
+
199
+ // Extend with additional styles
200
+ const PrimaryButton = styled(Button)`
201
+ background: #3b82f6;
202
+ color: white;
203
+ `;
204
+
205
+ // Chain extensions
206
+ const LargePrimaryButton = styled(PrimaryButton)`
207
+ padding: 1rem 2rem;
208
+ font-size: 1.25rem;
209
+ `;
210
+ ```
211
+
212
+ **CSS Cascade Order:**
213
+ When components are extended, classes are ordered correctly:
214
+
215
+ - Base styles first
216
+ - Extension styles second (override base)
217
+ - User className last (override all)
218
+
219
+ ```tsx
220
+ <LargePrimaryButton className="custom" />
221
+ // Renders: class="ss-base ss-primary ss-large custom"
222
+ ```
223
+
224
+ ### css Helper
225
+
226
+ Get a scoped class name for mixing with other classes:
227
+
228
+ ```tsx
229
+ import { css } from 'styled-static';
230
+
231
+ const activeClass = css`
232
+ outline: 2px solid blue;
233
+ `;
234
+
235
+ const highlightClass = css`
236
+ box-shadow: 0 0 10px yellow;
237
+ `;
238
+
239
+ // Mix with styled components
240
+ <Button className={isActive ? activeClass : ''}>
241
+ Conditional styling
242
+ </Button>
243
+
244
+ // Combine multiple classes
245
+ <div className={`${activeClass} ${highlightClass}`}>
246
+ Multiple classes
247
+ </div>
248
+ ```
249
+
250
+ ### keyframes
251
+
252
+ Create scoped keyframe animations. The animation name is hashed to avoid conflicts between components:
253
+
254
+ ```tsx
255
+ import { styled, keyframes } from "styled-static";
256
+
257
+ const spin = keyframes`
258
+ from { transform: rotate(0deg); }
259
+ to { transform: rotate(360deg); }
260
+ `;
261
+
262
+ const pulse = keyframes`
263
+ 0%, 100% { opacity: 1; }
264
+ 50% { opacity: 0.5; }
265
+ `;
266
+
267
+ const Spinner = styled.div`
268
+ width: 24px;
269
+ height: 24px;
270
+ border: 2px solid #3b82f6;
271
+ border-top-color: transparent;
272
+ border-radius: 50%;
273
+ animation: ${spin} 1s linear infinite;
274
+ `;
275
+
276
+ const PulsingDot = styled.div`
277
+ width: 8px;
278
+ height: 8px;
279
+ background: #10b981;
280
+ border-radius: 50%;
281
+ animation: ${pulse} 2s ease-in-out infinite;
282
+ `;
283
+ ```
284
+
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
+ ```
305
+
306
+ ### attrs
307
+
308
+ Set default HTML attributes on styled components using the `.attrs()` method:
309
+
310
+ ```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
+ const SubmitButton = styled.button.attrs({
322
+ type: 'submit',
323
+ 'aria-label': 'Submit form',
324
+ })`
325
+ padding: 0.5rem 1rem;
326
+ background: #3b82f6;
327
+ color: white;
328
+ `;
329
+
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
+ <SubmitButton>Send</SubmitButton>
335
+ // Renders: <button type="submit" aria-label="Submit form" class="ss-xyz789">Send</button>
336
+ ```
337
+
338
+ > **Note:** Unlike styled-components, attrs in styled-static must be static objects (no functions). For dynamic attributes, use regular props on your component.
339
+
340
+ ### cx Utility
341
+
342
+ Combine class names conditionally with the minimal `cx` utility (~40 bytes):
343
+
344
+ ```tsx
345
+ import { css, cx } from 'styled-static';
346
+
347
+ const activeClass = css`
348
+ color: blue;
349
+ `;
350
+
351
+ // Multiple class names
352
+ <div className={cx('base', 'active')} />
353
+ // → class="base active"
354
+
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"
363
+ ```
364
+
365
+ ### Global Styles
366
+
367
+ ```tsx
368
+ import { createGlobalStyle } from "styled-static";
369
+
370
+ const GlobalStyle = createGlobalStyle`
371
+ * {
372
+ box-sizing: border-box;
373
+ }
374
+
375
+ body {
376
+ margin: 0;
377
+ font-family: system-ui, sans-serif;
378
+ }
379
+
380
+ :root {
381
+ --color-primary: #3b82f6;
382
+ --color-text: #1a1a1a;
383
+ }
384
+ `;
385
+
386
+ // Render once at app root
387
+ createRoot(document.getElementById("root")!).render(
388
+ <StrictMode>
389
+ <GlobalStyle />
390
+ <App />
391
+ </StrictMode>
392
+ );
393
+ ```
394
+
395
+ > **Note:** The component renders nothing at runtime. All CSS is extracted and injected via imports.
396
+
397
+ ### Variants API
398
+
399
+ For type-safe variant handling, use `styledVariants` to create components with variant props, or `cssVariants` to get class functions.
400
+
401
+ > **Tip:** Wrap CSS strings in `css\`...\`` to get IDE syntax highlighting from the styled-components VSCode extension.
402
+
403
+ #### styledVariants
404
+
405
+ ```tsx
406
+ import { styledVariants, css } from "styled-static";
407
+
408
+ // With css`` for syntax highlighting (recommended)
409
+ const Button = styledVariants({
410
+ component: "button",
411
+ css: css`
412
+ padding: 0.5rem 1rem;
413
+ border: none;
414
+ border-radius: 4px;
415
+ cursor: pointer;
416
+ `,
417
+ variants: {
418
+ 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
+ `,
431
+ },
432
+ 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
+ `,
444
+ },
445
+ },
446
+ });
447
+
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>;
461
+ // Renders: <button class="ss-abc ss-abc--color-primary ss-abc--size-lg">
462
+ ```
463
+
464
+ #### cssVariants
465
+
466
+ ```tsx
467
+ import { cssVariants, css, cx } from 'styled-static';
468
+
469
+ // With css`` for syntax highlighting (recommended)
470
+ const badgeCss = cssVariants({
471
+ css: css`
472
+ padding: 0.25rem 0.5rem;
473
+ border-radius: 4px;
474
+ font-size: 0.75rem;
475
+ `,
476
+ variants: {
477
+ variant: {
478
+ info: css`background: #e0f2fe; color: #0369a1;`,
479
+ success: css`background: #dcfce7; color: #166534;`,
480
+ warning: css`background: #fef3c7; color: #92400e;`,
481
+ },
482
+ },
483
+ });
484
+
485
+ // Usage - returns class string
486
+ <span className={badgeCss({ variant: 'info' })}>Info</span>
487
+ // Returns: "ss-xyz ss-xyz--variant-info"
488
+
489
+ // Combine with cx for conditional classes
490
+ <span className={cx(badgeCss({ variant: 'info' }), isActive && activeClass)}>
491
+ Info
492
+ </span>
493
+ ```
494
+
495
+ ---
496
+
497
+ ## Features
498
+
499
+ ### Polymorphic `as` Prop
500
+
501
+ Render a styled component as a different HTML element:
502
+
503
+ ```tsx
504
+ const Button = styled.button`
505
+ padding: 0.5rem 1rem;
506
+ background: blue;
507
+ color: white;
508
+ `;
509
+
510
+ // Render as an anchor tag
511
+ <Button as="a" href="/link">
512
+ I'm a link styled as a button
513
+ </Button>;
514
+ ```
515
+
516
+ ### Transient Props
517
+
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:
519
+
520
+ ```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>
534
+ ```
535
+
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.
537
+
538
+ ### CSS Nesting
539
+
540
+ styled-static uses native CSS nesting (supported in all modern browsers):
541
+
542
+ ```tsx
543
+ const Card = styled.div`
544
+ padding: 1rem;
545
+ background: white;
546
+ border-radius: 8px;
547
+
548
+ /* Pseudo-classes */
549
+ &:hover {
550
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
551
+ }
552
+
553
+ /* Child selectors */
554
+ & h2 {
555
+ margin: 0 0 0.5rem;
556
+ }
557
+
558
+ /* Media queries */
559
+ @media (max-width: 640px) {
560
+ padding: 0.5rem;
561
+ }
562
+
563
+ /* Pseudo-elements */
564
+ &::before {
565
+ content: "";
566
+ position: absolute;
567
+ }
568
+ `;
569
+ ```
570
+
571
+ > **Tip:** Native CSS nesting means zero build-time processing. Your CSS is passed directly to the browser.
572
+
573
+ ---
574
+
575
+ ## Dynamic Styling
576
+
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
+ });
601
+
602
+ // Usage - variant props are type-safe
603
+ <Button color="primary">Click</Button>
604
+ <Button color="danger">Delete</Button>
605
+ ```
606
+
607
+ See the [Variants API](#variants-api) section for full documentation.
608
+
609
+ ### 2. Class Toggling with cx
610
+
611
+ ```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;
619
+ `;
620
+
621
+ const Button = styled.button`
622
+ padding: 0.5rem 1rem;
623
+ color: white;
624
+ `;
625
+
626
+ // Toggle between classes based on props/state
627
+ <Button className={cx(isPrimary ? primaryClass : dangerClass)}>Click</Button>;
628
+ ```
629
+
630
+ ### 3. Data Attributes
631
+
632
+ ```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
+ `;
647
+
648
+ <Button data-variant={variant}>Click</Button>;
649
+ ```
650
+
651
+ ### 4. CSS Variables
652
+
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
+ `;
659
+
660
+ <Button style={{ "--btn-bg": color, "--btn-color": textColor }}>Click</Button>;
661
+ ```
662
+
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
670
+
671
+ Use `createGlobalStyle` to define your theme tokens:
672
+
673
+ ```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
+ `;
706
+
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;
714
+
715
+ &:hover {
716
+ background: var(--color-accent);
717
+ }
718
+ `;
719
+ ```
720
+
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);
747
+
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
+ ```
755
+
756
+ ### Theme Toggle Example
757
+
758
+ ```tsx
759
+ import { getTheme, setTheme } from "styled-static";
760
+
761
+ function ThemeToggle() {
762
+ const toggleTheme = () => {
763
+ const current = getTheme();
764
+ setTheme(current === "dark" ? "light" : "dark");
765
+ };
766
+
767
+ return <button onClick={toggleTheme}>Toggle Theme</button>;
768
+ }
769
+
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
+ }
780
+ ```
781
+
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
+ ```
809
+
810
+ ### API Reference
811
+
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. |
818
+
819
+ #### initTheme Options
820
+
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
+ });
828
+ ```
829
+
830
+ ---
831
+
832
+ ## How It Works
833
+
834
+ styled-static transforms your code at build time:
835
+
836
+ ```tsx
837
+ // You write:
838
+ const Button = styled.button`
839
+ padding: 1rem;
840
+ background: blue;
841
+ `;
842
+
843
+ // Becomes:
844
+ import "styled-static:abc123-0.css";
845
+ const Button = __styled("button", "ss-def456", "Button");
846
+
847
+ // CSS extracted to static file:
848
+ .ss-def456 {
849
+ padding: 1rem;
850
+ background: blue;
851
+ }
852
+ ```
853
+
854
+ ---
855
+
856
+ ## Configuration
857
+
858
+ ```ts
859
+ styledStatic({
860
+ // Prefix for generated class names (default: 'ss')
861
+ classPrefix: "my-app",
862
+ });
863
+ ```
864
+
865
+ ---
866
+
867
+ ## TypeScript
868
+
869
+ Full type inference is provided:
870
+
871
+ ```tsx
872
+ const Button = styled.button`...`;
873
+
874
+ // ✅ Type-safe: button props are available
875
+ <Button type="submit" disabled>Submit</Button>
876
+
877
+ // ✅ Type-safe: as prop changes available props
878
+ <Button as="a" href="/link">Link</Button>
879
+
880
+ // ✅ Transient props are typed
881
+ <Button $primary={true}>Primary</Button>
882
+ ```
883
+
884
+ ---
885
+
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
+ ## Zero Dependencies
895
+
896
+ The plugin has **ZERO** direct dependencies! 🎉
897
+
898
+ It relies on:
899
+
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
904
+
905
+ For faster CSS processing and automatic vendor prefixes, install Lightning CSS:
906
+
907
+ ```bash
908
+ npm install lightningcss
909
+ ```
910
+
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
+ ```
919
+
920
+ ---
921
+
922
+ ## Comparison
923
+
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+ |
932
+
933
+ ---
934
+
935
+ ## VS Code Support
936
+
937
+ For syntax highlighting in template literals, install the [vscode-styled-components](https://marketplace.visualstudio.com/items?itemName=styled-components.vscode-styled-components) extension.
938
+
939
+ ---
940
+
941
+ ## License
942
+
943
+ MIT