@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 +249 -413
- package/dist/hash.js +1 -46
- package/dist/index.d.ts +159 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -133
- package/dist/index.js.map +1 -1
- package/dist/runtime/index.d.ts +27 -0
- package/dist/runtime/index.d.ts.map +1 -0
- package/dist/runtime/index.js +1 -0
- package/dist/runtime/index.js.map +1 -0
- package/dist/theme.d.ts +122 -0
- package/dist/theme.d.ts.map +1 -0
- package/dist/theme.js +1 -0
- package/dist/theme.js.map +1 -0
- package/dist/types.d.ts +243 -23
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +0 -2
- package/dist/vite.d.ts +4 -73
- package/dist/vite.d.ts.map +1 -1
- package/dist/vite.js +38 -395
- package/dist/vite.js.map +1 -1
- package/package.json +14 -5
- package/dist/runtime.d.ts +0 -58
- package/dist/runtime.d.ts.map +0 -1
- package/dist/runtime.js +0 -99
- package/dist/runtime.js.map +0 -1
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:
|
|
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
|
-
|
|
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
|
-
- ⚡ **
|
|
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** - ~
|
|
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
|
|
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
|
-
|
|
108
|
+
// Combine classes conditionally
|
|
109
|
+
<div className={cx("base", isActive && activeClass)} />;
|
|
111
110
|
|
|
112
|
-
|
|
111
|
+
// Default attributes
|
|
112
|
+
const PasswordInput = styled.input.attrs({ type: "password" })`
|
|
113
|
+
padding: 0.5rem 1rem;
|
|
114
|
+
`;
|
|
113
115
|
|
|
114
|
-
|
|
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
|
-
|
|
122
|
+
---
|
|
117
123
|
|
|
118
|
-
|
|
124
|
+
## Table of Contents
|
|
119
125
|
|
|
120
|
-
-
|
|
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
|
-
##
|
|
133
|
+
## Why styled-static?
|
|
125
134
|
|
|
126
|
-
|
|
135
|
+
- 🌐 **CSS evolved.** Native nesting, CSS variables, container queries—the 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
|
-
|
|
142
|
+
---
|
|
129
143
|
|
|
130
|
-
|
|
144
|
+
## What We Don't Do
|
|
131
145
|
|
|
132
|
-
-
|
|
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
|
-
|
|
152
|
+
Each constraint removes complexity—no 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
|
|
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 {
|
|
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
|
-
|
|
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
|
|
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">
|
|
320
|
+
// Renders: <button type="submit" aria-label="Submit form" class="ss-xyz789">
|
|
336
321
|
```
|
|
337
322
|
|
|
338
|
-
> **Note:**
|
|
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
|
|
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
|
-
//
|
|
356
|
-
|
|
357
|
-
// →
|
|
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 {
|
|
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
|
-
|
|
414
|
-
|
|
415
|
-
|
|
385
|
+
background: gray;
|
|
386
|
+
color: white;
|
|
387
|
+
font-size: 1rem;
|
|
416
388
|
`,
|
|
417
389
|
variants: {
|
|
418
390
|
color: {
|
|
419
|
-
primary: css`
|
|
420
|
-
|
|
421
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
###
|
|
441
|
+
### Polymorphism with withComponent
|
|
500
442
|
|
|
501
|
-
Render
|
|
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
|
-
//
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
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
|
-
|
|
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
|
-
|
|
473
|
+
Every styled component exposes a static `.className` property for manual composition:
|
|
519
474
|
|
|
520
475
|
```tsx
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
//
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
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
|
-
|
|
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
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
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
|
-
|
|
603
|
-
<Button color="primary">Click</Button>
|
|
604
|
-
<Button color="danger">Delete</Button>
|
|
605
|
-
```
|
|
539
|
+
---
|
|
606
540
|
|
|
607
|
-
|
|
541
|
+
## Theming
|
|
608
542
|
|
|
609
|
-
|
|
543
|
+
CSS-first theming with CSS variables and `data-theme` attributes:
|
|
610
544
|
|
|
611
545
|
```tsx
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
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
|
|
622
|
-
|
|
623
|
-
color:
|
|
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
|
-
###
|
|
558
|
+
### Theme Helpers
|
|
631
559
|
|
|
632
560
|
```tsx
|
|
633
|
-
|
|
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
|
-
|
|
649
|
-
|
|
563
|
+
// Initialize (reads localStorage → system preference → default)
|
|
564
|
+
initTheme({ defaultTheme: "light", useSystemPreference: true });
|
|
650
565
|
|
|
651
|
-
|
|
566
|
+
// Switch themes
|
|
567
|
+
setTheme("dark"); // persists to localStorage
|
|
568
|
+
setTheme("pokemon", false); // no persist (preview)
|
|
652
569
|
|
|
653
|
-
|
|
654
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
666
|
-
|
|
667
|
-
|
|
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
|
-
|
|
586
|
+
---
|
|
672
587
|
|
|
673
|
-
|
|
674
|
-
import { createGlobalStyle, styled } from "styled-static";
|
|
588
|
+
## Troubleshooting
|
|
675
589
|
|
|
676
|
-
|
|
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
|
-
|
|
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
|
-
|
|
716
|
-
|
|
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
|
-
|
|
599
|
+
Add the package to Vite's `optimizeDeps.include` in your Storybook config:
|
|
722
600
|
|
|
723
|
-
|
|
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
|
-
|
|
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
|
-
|
|
740
|
-
const current = getTheme(); // 'light' | 'dark' | 'pokemon' | etc.
|
|
618
|
+
---
|
|
741
619
|
|
|
742
|
-
|
|
743
|
-
setTheme("dark");
|
|
620
|
+
## How It Works
|
|
744
621
|
|
|
745
|
-
|
|
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
|
-
|
|
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
|
-
|
|
626
|
+
When you write a styled component, the Vite plugin intercepts your code and performs AST-based transformation:
|
|
757
627
|
|
|
758
628
|
```tsx
|
|
759
|
-
|
|
629
|
+
// 1. What you write:
|
|
630
|
+
import { styled } from "@alex.radulescu/styled-static";
|
|
760
631
|
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
632
|
+
const Button = styled.button`
|
|
633
|
+
padding: 1rem;
|
|
634
|
+
background: blue;
|
|
635
|
+
color: white;
|
|
636
|
+
`;
|
|
766
637
|
|
|
767
|
-
|
|
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
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
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
|
-
|
|
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
|
-
|
|
651
|
+
### Virtual CSS Modules
|
|
785
652
|
|
|
786
|
-
|
|
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
|
-
|
|
807
|
-
|
|
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
|
-
###
|
|
669
|
+
### Minimal Runtime
|
|
811
670
|
|
|
812
|
-
|
|
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
|
-
|
|
673
|
+
| Module | Minified | Brotli |
|
|
674
|
+
| ---------------- | -------- | ------ |
|
|
675
|
+
| runtime/index.js | **45 B** | 50 B |
|
|
820
676
|
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
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
|
-
|
|
686
|
+
### Zero-Runtime Features
|
|
833
687
|
|
|
834
|
-
|
|
688
|
+
Some features have literally zero runtime cost because they're completely replaced at build time:
|
|
835
689
|
|
|
836
690
|
```tsx
|
|
837
|
-
//
|
|
838
|
-
const
|
|
839
|
-
|
|
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
|
-
//
|
|
844
|
-
|
|
845
|
-
const
|
|
695
|
+
// Global styles - zero runtime (just CSS import)
|
|
696
|
+
const GlobalStyles = createGlobalStyle`* { box-sizing: border-box; }`;
|
|
697
|
+
// Generated: const GlobalStyles = () => null;
|
|
846
698
|
|
|
847
|
-
//
|
|
848
|
-
|
|
849
|
-
|
|
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:
|
|
878
|
-
|
|
727
|
+
// ✅ Type-safe: withComponent infers props from target component
|
|
728
|
+
const LinkButton = withComponent(Link, Button);
|
|
729
|
+
<LinkButton to="/path">Link</LinkButton>
|
|
879
730
|
|
|
880
|
-
// ✅
|
|
881
|
-
|
|
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
|
-
|
|
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
|
-
|
|
901
|
-
- **Vite's CSS pipeline** - Handles the virtual CSS modules
|
|
902
|
-
|
|
903
|
-
### Optional: Lightning CSS
|
|
741
|
+
---
|
|
904
742
|
|
|
905
|
-
|
|
743
|
+
## Comparison
|
|
906
744
|
|
|
907
|
-
|
|
908
|
-
npm install lightningcss
|
|
909
|
-
```
|
|
745
|
+
**Legend:** ✓ Yes | ◐ Partial | ✗ No
|
|
910
746
|
|
|
911
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
765
|
+
## VS Code Support
|
|
923
766
|
|
|
924
|
-
|
|
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
|
-
##
|
|
771
|
+
## Inspiration
|
|
936
772
|
|
|
937
|
-
|
|
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
|
|