@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/LICENSE +21 -0
- package/README.md +943 -0
- package/dist/hash.d.ts +9 -0
- package/dist/hash.d.ts.map +1 -0
- package/dist/hash.js +46 -0
- package/dist/hash.js.map +1 -0
- package/dist/index.d.ts +118 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +133 -0
- package/dist/index.js.map +1 -0
- package/dist/runtime.d.ts +58 -0
- package/dist/runtime.d.ts.map +1 -0
- package/dist/runtime.js +99 -0
- package/dist/runtime.js.map +1 -0
- package/dist/types.d.ts +57 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/vite.d.ts +87 -0
- package/dist/vite.d.ts.map +1 -0
- package/dist/vite.js +395 -0
- package/dist/vite.js.map +1 -0
- package/package.json +68 -0
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
|