@stylix/core 6.2.1 → 6.3.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 +140 -280
- package/dist/index.d.ts +83 -59
- package/dist/index.js +276 -195
- package/dist/index.js.map +1 -1
- package/docs/CLAUDE_CONTEXT.md +156 -0
- package/docs/cheatsheet.md +354 -0
- package/docs/patterns.md +754 -0
- package/docs/performance.md +291 -0
- package/docs/philosophy.md +168 -0
- package/package.json +23 -20
- package/tsconfig.json +0 -0
package/docs/patterns.md
ADDED
|
@@ -0,0 +1,754 @@
|
|
|
1
|
+
# Common Patterns
|
|
2
|
+
|
|
3
|
+
This guide covers practical patterns for using Stylix effectively. These patterns have emerged from real-world usage and represent idiomatic Stylix code.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Creating Styled Components
|
|
8
|
+
|
|
9
|
+
### Basic Wrapper Component
|
|
10
|
+
|
|
11
|
+
The simplest pattern: wrap a Stylix element and spread style props.
|
|
12
|
+
|
|
13
|
+
```tsx
|
|
14
|
+
import $, { StylixProps } from '@stylix/core';
|
|
15
|
+
|
|
16
|
+
interface ButtonProps extends StylixProps {
|
|
17
|
+
children: React.ReactNode;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function Button({ children, ...styles }: ButtonProps) {
|
|
21
|
+
return (
|
|
22
|
+
<$.button
|
|
23
|
+
padding="10px 20px"
|
|
24
|
+
border="none"
|
|
25
|
+
border-radius={4}
|
|
26
|
+
cursor="pointer"
|
|
27
|
+
background="#0066cc"
|
|
28
|
+
color="white"
|
|
29
|
+
{...styles} // Allows parent to override any style
|
|
30
|
+
>
|
|
31
|
+
{children}
|
|
32
|
+
</$.button>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Usage
|
|
37
|
+
<Button>Default</Button>
|
|
38
|
+
<Button background="green">Green Button</Button>
|
|
39
|
+
<Button padding="20px 40px" font-size={18}>Large Button</Button>
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
The `{...styles}` spread is key—it lets parent components override any default style.
|
|
43
|
+
|
|
44
|
+
### Component with Variants
|
|
45
|
+
|
|
46
|
+
Use component props to drive style variations:
|
|
47
|
+
|
|
48
|
+
```tsx
|
|
49
|
+
interface ButtonProps extends StylixProps {
|
|
50
|
+
variant?: 'primary' | 'secondary' | 'danger';
|
|
51
|
+
size?: 'sm' | 'md' | 'lg';
|
|
52
|
+
children: React.ReactNode;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function Button({
|
|
56
|
+
variant = 'primary',
|
|
57
|
+
size = 'md',
|
|
58
|
+
children,
|
|
59
|
+
...styles
|
|
60
|
+
}: ButtonProps) {
|
|
61
|
+
const backgrounds = {
|
|
62
|
+
primary: '#0066cc',
|
|
63
|
+
secondary: '#666',
|
|
64
|
+
danger: '#cc0000'
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const paddings = {
|
|
68
|
+
sm: '6px 12px',
|
|
69
|
+
md: '10px 20px',
|
|
70
|
+
lg: '14px 28px'
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const fontSizes = {
|
|
74
|
+
sm: 12,
|
|
75
|
+
md: 14,
|
|
76
|
+
lg: 16
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
return (
|
|
80
|
+
<$.button
|
|
81
|
+
padding={paddings[size]}
|
|
82
|
+
font-size={fontSizes[size]}
|
|
83
|
+
background={backgrounds[variant]}
|
|
84
|
+
color="white"
|
|
85
|
+
border="none"
|
|
86
|
+
border-radius={4}
|
|
87
|
+
cursor="pointer"
|
|
88
|
+
{...styles}
|
|
89
|
+
>
|
|
90
|
+
{children}
|
|
91
|
+
</$.button>
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Usage
|
|
96
|
+
<Button variant="danger" size="lg">Delete</Button>
|
|
97
|
+
<Button variant="secondary">Cancel</Button>
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### Component with Complex Default Styles
|
|
101
|
+
|
|
102
|
+
When defaults include pseudo-classes or media queries, use `$css` with array merging:
|
|
103
|
+
|
|
104
|
+
```tsx
|
|
105
|
+
interface CardProps extends StylixProps {
|
|
106
|
+
elevated?: boolean;
|
|
107
|
+
children: React.ReactNode;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function Card({ elevated = false, children, ...styles }: CardProps) {
|
|
111
|
+
return (
|
|
112
|
+
<$.div
|
|
113
|
+
background="white"
|
|
114
|
+
border-radius={8}
|
|
115
|
+
padding={16}
|
|
116
|
+
{...styles}
|
|
117
|
+
$css={[
|
|
118
|
+
// Default complex styles
|
|
119
|
+
{
|
|
120
|
+
transition: 'box-shadow 0.2s, transform 0.2s',
|
|
121
|
+
'&:hover': elevated ? {
|
|
122
|
+
boxShadow: '0 8px 24px rgba(0,0,0,0.12)',
|
|
123
|
+
transform: 'translateY(-2px)'
|
|
124
|
+
} : {}
|
|
125
|
+
},
|
|
126
|
+
// Allow parent overrides
|
|
127
|
+
styles.$css
|
|
128
|
+
]}
|
|
129
|
+
>
|
|
130
|
+
{children}
|
|
131
|
+
</$.div>
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Parent can override hover behavior:
|
|
136
|
+
<Card
|
|
137
|
+
elevated
|
|
138
|
+
$css={{
|
|
139
|
+
'&:hover': { transform: 'none' } // Disable hover lift
|
|
140
|
+
}}
|
|
141
|
+
/>
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
**Important:** When merging `$css`, parent styles (`styles.$css`) must come last to take precedence.
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
## Responsive Design
|
|
149
|
+
|
|
150
|
+
### Setting Up Breakpoints
|
|
151
|
+
|
|
152
|
+
Define breakpoints once at the app root:
|
|
153
|
+
|
|
154
|
+
```tsx
|
|
155
|
+
import $, { StylixProvider } from '@stylix/core';
|
|
156
|
+
|
|
157
|
+
const mediaConfig = {
|
|
158
|
+
// Mobile-first approach
|
|
159
|
+
sm: styles => ({ '@media (min-width: 640px)': styles }),
|
|
160
|
+
md: styles => ({ '@media (min-width: 768px)': styles }),
|
|
161
|
+
lg: styles => ({ '@media (min-width: 1024px)': styles }),
|
|
162
|
+
xl: styles => ({ '@media (min-width: 1280px)': styles }),
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
// Or desktop-first
|
|
166
|
+
const mediaConfigDesktopFirst = {
|
|
167
|
+
mobile: styles => ({ '@media (max-width: 767px)': styles }),
|
|
168
|
+
tablet: styles => ({ '@media (max-width: 1023px)': styles }),
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
function App() {
|
|
172
|
+
return (
|
|
173
|
+
<StylixProvider media={mediaConfig}>
|
|
174
|
+
<AppContent />
|
|
175
|
+
</StylixProvider>
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
### Using Responsive Props
|
|
181
|
+
|
|
182
|
+
```tsx
|
|
183
|
+
// Individual properties
|
|
184
|
+
<$.div
|
|
185
|
+
padding={{ default: 24, md: 16, sm: 12 }}
|
|
186
|
+
font-size={{ default: 18, sm: 14 }}
|
|
187
|
+
/>
|
|
188
|
+
|
|
189
|
+
// In $css blocks
|
|
190
|
+
<$.div
|
|
191
|
+
$css={{
|
|
192
|
+
display: 'grid',
|
|
193
|
+
gridTemplateColumns: 'repeat(3, 1fr)',
|
|
194
|
+
gap: 24,
|
|
195
|
+
md: {
|
|
196
|
+
gridTemplateColumns: 'repeat(2, 1fr)',
|
|
197
|
+
gap: 16
|
|
198
|
+
},
|
|
199
|
+
sm: {
|
|
200
|
+
gridTemplateColumns: '1fr',
|
|
201
|
+
gap: 12
|
|
202
|
+
}
|
|
203
|
+
}}
|
|
204
|
+
/>
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
### Responsive Layout Component
|
|
208
|
+
|
|
209
|
+
```tsx
|
|
210
|
+
import $, { StylixProps, StylixValue } from '@stylix/core';
|
|
211
|
+
|
|
212
|
+
interface StackProps extends StylixProps {
|
|
213
|
+
direction?: StylixValue<'row' | 'column'>;
|
|
214
|
+
gap?: StylixValue<number>;
|
|
215
|
+
children: React.ReactNode;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function Stack({
|
|
219
|
+
direction = 'column',
|
|
220
|
+
gap = 16,
|
|
221
|
+
children,
|
|
222
|
+
...styles
|
|
223
|
+
}: StackProps) {
|
|
224
|
+
return (
|
|
225
|
+
<$.div
|
|
226
|
+
display="flex"
|
|
227
|
+
flex-direction={direction}
|
|
228
|
+
gap={gap}
|
|
229
|
+
{...styles}
|
|
230
|
+
>
|
|
231
|
+
{children}
|
|
232
|
+
</$.div>
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Usage
|
|
237
|
+
<Stack
|
|
238
|
+
direction={{ default: 'row', mobile: 'column' }}
|
|
239
|
+
gap={{ default: 24, mobile: 12 }}
|
|
240
|
+
>
|
|
241
|
+
<Item />
|
|
242
|
+
<Item />
|
|
243
|
+
</Stack>
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
---
|
|
247
|
+
|
|
248
|
+
## Theme Support
|
|
249
|
+
|
|
250
|
+
### Light/Dark Mode via Media Config
|
|
251
|
+
|
|
252
|
+
```tsx
|
|
253
|
+
<StylixProvider
|
|
254
|
+
media={{
|
|
255
|
+
// Theme modes (assumes data-theme attribute on html/body)
|
|
256
|
+
light: styles => ({ '[data-theme="light"] &': styles }),
|
|
257
|
+
dark: styles => ({ '[data-theme="dark"] &': styles }),
|
|
258
|
+
|
|
259
|
+
// Or using prefers-color-scheme
|
|
260
|
+
prefersDark: styles => ({ '@media (prefers-color-scheme: dark)': styles }),
|
|
261
|
+
|
|
262
|
+
// Breakpoints
|
|
263
|
+
mobile: styles => ({ '@media (max-width: 767px)': styles }),
|
|
264
|
+
}}
|
|
265
|
+
>
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
### Using Theme Styles
|
|
269
|
+
|
|
270
|
+
```tsx
|
|
271
|
+
<$.div
|
|
272
|
+
background={{ light: 'white', dark: '#1a1a1a' }}
|
|
273
|
+
color={{ light: '#333', dark: '#e8e8e8' }}
|
|
274
|
+
border-color={{ light: '#ddd', dark: '#444' }}
|
|
275
|
+
/>
|
|
276
|
+
|
|
277
|
+
// Nested: dark mode + responsive
|
|
278
|
+
<$.div
|
|
279
|
+
padding={{
|
|
280
|
+
default: 24,
|
|
281
|
+
mobile: 16,
|
|
282
|
+
dark: {
|
|
283
|
+
default: 20,
|
|
284
|
+
mobile: 12
|
|
285
|
+
}
|
|
286
|
+
}}
|
|
287
|
+
/>
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
### Theme with CSS Variables
|
|
291
|
+
|
|
292
|
+
For more complex theming, combine Stylix with CSS variables defined in plain CSS:
|
|
293
|
+
|
|
294
|
+
```css
|
|
295
|
+
/* styles.css */
|
|
296
|
+
:root {
|
|
297
|
+
--color-bg: white;
|
|
298
|
+
--color-text: #333;
|
|
299
|
+
--color-primary: #0066cc;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
[data-theme="dark"] {
|
|
303
|
+
--color-bg: #1a1a1a;
|
|
304
|
+
--color-text: #e8e8e8;
|
|
305
|
+
--color-primary: #4d9fff;
|
|
306
|
+
}
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
```tsx
|
|
310
|
+
// Components use the variables
|
|
311
|
+
<$.div
|
|
312
|
+
background="var(--color-bg)"
|
|
313
|
+
color="var(--color-text)"
|
|
314
|
+
/>
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
---
|
|
318
|
+
|
|
319
|
+
## Layout Patterns
|
|
320
|
+
|
|
321
|
+
### Container
|
|
322
|
+
|
|
323
|
+
```tsx
|
|
324
|
+
function Container({ children, ...styles }: StylixProps & { children: React.ReactNode }) {
|
|
325
|
+
return (
|
|
326
|
+
<$.div
|
|
327
|
+
max-width={1200}
|
|
328
|
+
margin="0 auto"
|
|
329
|
+
padding={{ default: '0 24px', mobile: '0 16px' }}
|
|
330
|
+
{...styles}
|
|
331
|
+
>
|
|
332
|
+
{children}
|
|
333
|
+
</$.div>
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
### Flex Utilities
|
|
339
|
+
|
|
340
|
+
```tsx
|
|
341
|
+
// Center content
|
|
342
|
+
<$.div
|
|
343
|
+
display="flex"
|
|
344
|
+
justify-content="center"
|
|
345
|
+
align-items="center"
|
|
346
|
+
/>
|
|
347
|
+
|
|
348
|
+
// Space between
|
|
349
|
+
<$.div
|
|
350
|
+
display="flex"
|
|
351
|
+
justify-content="space-between"
|
|
352
|
+
align-items="center"
|
|
353
|
+
/>
|
|
354
|
+
|
|
355
|
+
// As a reusable component
|
|
356
|
+
import $, { StylixProps, StylixValue } from '@stylix/core';
|
|
357
|
+
|
|
358
|
+
function Flex({
|
|
359
|
+
justify = 'flex-start',
|
|
360
|
+
align = 'stretch',
|
|
361
|
+
direction = 'row',
|
|
362
|
+
gap,
|
|
363
|
+
wrap = 'nowrap',
|
|
364
|
+
children,
|
|
365
|
+
...styles
|
|
366
|
+
}: StylixProps & {
|
|
367
|
+
justify?: StylixValue<string>;
|
|
368
|
+
align?: StylixValue<string>;
|
|
369
|
+
direction?: StylixValue<'row' | 'column'>;
|
|
370
|
+
gap?: StylixValue<number>;
|
|
371
|
+
wrap?: StylixValue<'wrap' | 'nowrap' | 'wrap-reverse'>;
|
|
372
|
+
children: React.ReactNode;
|
|
373
|
+
}) {
|
|
374
|
+
return (
|
|
375
|
+
<$.div
|
|
376
|
+
display="flex"
|
|
377
|
+
flex-direction={direction}
|
|
378
|
+
justify-content={justify}
|
|
379
|
+
align-items={align}
|
|
380
|
+
gap={gap}
|
|
381
|
+
flex-wrap={wrap}
|
|
382
|
+
{...styles}
|
|
383
|
+
>
|
|
384
|
+
{children}
|
|
385
|
+
</$.div>
|
|
386
|
+
);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// Usage
|
|
390
|
+
<Flex justify="space-between" align="center" gap={16}>
|
|
391
|
+
<Logo />
|
|
392
|
+
<Nav />
|
|
393
|
+
</Flex>
|
|
394
|
+
|
|
395
|
+
// With responsive props
|
|
396
|
+
<Flex
|
|
397
|
+
direction={{ default: 'row', mobile: 'column' }}
|
|
398
|
+
gap={{ default: 24, mobile: 12 }}
|
|
399
|
+
wrap={{ default: 'nowrap', mobile: 'wrap' }}
|
|
400
|
+
>
|
|
401
|
+
<Sidebar />
|
|
402
|
+
<MainContent />
|
|
403
|
+
</Flex>
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
### Grid Layout
|
|
407
|
+
|
|
408
|
+
```tsx
|
|
409
|
+
function ProductGrid({ products }: { products: Product[] }) {
|
|
410
|
+
return (
|
|
411
|
+
<$.div
|
|
412
|
+
display="grid"
|
|
413
|
+
grid-template-columns={{
|
|
414
|
+
default: 'repeat(4, 1fr)',
|
|
415
|
+
lg: 'repeat(3, 1fr)',
|
|
416
|
+
md: 'repeat(2, 1fr)',
|
|
417
|
+
sm: '1fr'
|
|
418
|
+
}}
|
|
419
|
+
gap={{ default: 24, sm: 16 }}
|
|
420
|
+
>
|
|
421
|
+
{products.map(product => (
|
|
422
|
+
<ProductCard key={product.id} product={product} />
|
|
423
|
+
))}
|
|
424
|
+
</$.div>
|
|
425
|
+
);
|
|
426
|
+
}
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
---
|
|
430
|
+
|
|
431
|
+
## Interactive States
|
|
432
|
+
|
|
433
|
+
### Hover, Focus, Active
|
|
434
|
+
|
|
435
|
+
```tsx
|
|
436
|
+
<$.button
|
|
437
|
+
background="#0066cc"
|
|
438
|
+
color="white"
|
|
439
|
+
padding="10px 20px"
|
|
440
|
+
border="none"
|
|
441
|
+
$css={{
|
|
442
|
+
transition: 'all 0.2s',
|
|
443
|
+
'&:hover': {
|
|
444
|
+
background: '#0052a3'
|
|
445
|
+
},
|
|
446
|
+
'&:focus': {
|
|
447
|
+
outline: '2px solid #0066cc',
|
|
448
|
+
outlineOffset: 2
|
|
449
|
+
},
|
|
450
|
+
'&:active': {
|
|
451
|
+
transform: 'scale(0.98)'
|
|
452
|
+
},
|
|
453
|
+
'&:disabled': {
|
|
454
|
+
opacity: 0.5,
|
|
455
|
+
cursor: 'not-allowed'
|
|
456
|
+
}
|
|
457
|
+
}}
|
|
458
|
+
>
|
|
459
|
+
Click me
|
|
460
|
+
</$.button>
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
### Focus-Visible (Keyboard Focus Only)
|
|
464
|
+
|
|
465
|
+
```tsx
|
|
466
|
+
<$.button
|
|
467
|
+
$css={{
|
|
468
|
+
outline: 'none',
|
|
469
|
+
'&:focus-visible': {
|
|
470
|
+
outline: '2px solid #0066cc',
|
|
471
|
+
outlineOffset: 2
|
|
472
|
+
}
|
|
473
|
+
}}
|
|
474
|
+
/>
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
### Group Hover (Parent Hover Affects Child)
|
|
478
|
+
|
|
479
|
+
```tsx
|
|
480
|
+
<$.div
|
|
481
|
+
$css={{
|
|
482
|
+
'&:hover .icon': {
|
|
483
|
+
transform: 'translateX(4px)'
|
|
484
|
+
}
|
|
485
|
+
}}
|
|
486
|
+
>
|
|
487
|
+
<$.span>Read more</$.span>
|
|
488
|
+
<$.span className="icon" transition="transform 0.2s">→</$.span>
|
|
489
|
+
</$.div>
|
|
490
|
+
```
|
|
491
|
+
|
|
492
|
+
---
|
|
493
|
+
|
|
494
|
+
## Animations
|
|
495
|
+
|
|
496
|
+
### Keyframe Animations
|
|
497
|
+
|
|
498
|
+
```tsx
|
|
499
|
+
import { useKeyframes } from '@stylix/core';
|
|
500
|
+
|
|
501
|
+
function FadeIn({ children }) {
|
|
502
|
+
const fadeIn = useKeyframes({
|
|
503
|
+
from: { opacity: 0, transform: 'translateY(10px)' },
|
|
504
|
+
to: { opacity: 1, transform: 'translateY(0)' }
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
return (
|
|
508
|
+
<$.div animation={`${fadeIn} 0.3s ease-out`}>
|
|
509
|
+
{children}
|
|
510
|
+
</$.div>
|
|
511
|
+
);
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
function Spinner() {
|
|
515
|
+
const spin = useKeyframes({
|
|
516
|
+
from: { transform: 'rotate(0deg)' },
|
|
517
|
+
to: { transform: 'rotate(360deg)' }
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
return (
|
|
521
|
+
<$.div
|
|
522
|
+
width={24}
|
|
523
|
+
height={24}
|
|
524
|
+
border="3px solid #eee"
|
|
525
|
+
border-top-color="#0066cc"
|
|
526
|
+
border-radius="50%"
|
|
527
|
+
animation={`${spin} 0.8s linear infinite`}
|
|
528
|
+
/>
|
|
529
|
+
);
|
|
530
|
+
}
|
|
531
|
+
```
|
|
532
|
+
|
|
533
|
+
### Transition-Based Animation
|
|
534
|
+
|
|
535
|
+
For simple state changes, transitions are often simpler:
|
|
536
|
+
|
|
537
|
+
```tsx
|
|
538
|
+
<$.div
|
|
539
|
+
opacity={isVisible ? 1 : 0}
|
|
540
|
+
transform={isVisible ? 'translateY(0)' : 'translateY(10px)'}
|
|
541
|
+
transition="opacity 0.3s, transform 0.3s"
|
|
542
|
+
/>
|
|
543
|
+
```
|
|
544
|
+
|
|
545
|
+
---
|
|
546
|
+
|
|
547
|
+
## Working with Third-Party Components
|
|
548
|
+
|
|
549
|
+
### Using `$el` with External Components
|
|
550
|
+
|
|
551
|
+
Pass an element instance to `$el` to apply Stylix styles. The styles are collected and a `className` prop is added to the element, so it must accept `className`.
|
|
552
|
+
|
|
553
|
+
```tsx
|
|
554
|
+
import { ExternalComponent } from 'some-library';
|
|
555
|
+
|
|
556
|
+
<$
|
|
557
|
+
$el={<ExternalComponent customProp="value" />}
|
|
558
|
+
color="red"
|
|
559
|
+
padding={16}
|
|
560
|
+
/>
|
|
561
|
+
```
|
|
562
|
+
|
|
563
|
+
### Using `$render` for Full Control
|
|
564
|
+
|
|
565
|
+
```tsx
|
|
566
|
+
import { ComplexComponent } from 'some-library';
|
|
567
|
+
|
|
568
|
+
<$
|
|
569
|
+
padding={16}
|
|
570
|
+
margin={8}
|
|
571
|
+
$render={(className) => (
|
|
572
|
+
<ComplexComponent
|
|
573
|
+
className={className}
|
|
574
|
+
onSomething={handler}
|
|
575
|
+
config={config}
|
|
576
|
+
/>
|
|
577
|
+
)}
|
|
578
|
+
/>
|
|
579
|
+
```
|
|
580
|
+
|
|
581
|
+
### Wrapping for Reuse
|
|
582
|
+
|
|
583
|
+
When wrapping a third-party component, you often want to accept both style props and the component's own props. Stylix automatically passes unrecognized props through to the `$el` element.
|
|
584
|
+
|
|
585
|
+
Use `Extends` to type the combined props (it handles conflicts by letting later types override earlier ones):
|
|
586
|
+
|
|
587
|
+
```tsx
|
|
588
|
+
import $, { Extends, StylixProps } from '@stylix/core';
|
|
589
|
+
import { Dialog as RadixDialog } from '@radix-ui/react-dialog';
|
|
590
|
+
|
|
591
|
+
type DialogProps = Extends<
|
|
592
|
+
RadixDialog.ContentProps, // Third-party props first
|
|
593
|
+
StylixProps, // Style props last (later types override earlier)
|
|
594
|
+
{ children: React.ReactNode }
|
|
595
|
+
>;
|
|
596
|
+
|
|
597
|
+
function Dialog({ $css, children, ...allProps }: DialogProps) {
|
|
598
|
+
return (
|
|
599
|
+
<$
|
|
600
|
+
$el={<RadixDialog.Content />}
|
|
601
|
+
background="white"
|
|
602
|
+
border-radius={8}
|
|
603
|
+
padding={24}
|
|
604
|
+
box-shadow="0 10px 40px rgba(0,0,0,0.2)"
|
|
605
|
+
{...allProps}
|
|
606
|
+
$css={[{ '&:focus': { outline: 'none' } }, $css]}
|
|
607
|
+
>
|
|
608
|
+
{children}
|
|
609
|
+
</$>
|
|
610
|
+
);
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
// Usage: accepts both style props and RadixDialog props
|
|
614
|
+
<Dialog onOpenAutoFocus={handleFocus} padding={32} max-width={600}>
|
|
615
|
+
Content
|
|
616
|
+
</Dialog>
|
|
617
|
+
```
|
|
618
|
+
|
|
619
|
+
**Handling prop name conflicts:** If a component prop conflicts with a CSS property name (rare), put the third-party props last in `Extends` so its `color` type wins, then rename the CSS version:
|
|
620
|
+
|
|
621
|
+
```tsx
|
|
622
|
+
type WidgetProps = Extends<
|
|
623
|
+
StylixProps,
|
|
624
|
+
ThirdPartyProps, // Last, so its `color` type wins
|
|
625
|
+
{ cssColor?: string } // Renamed CSS color prop
|
|
626
|
+
>;
|
|
627
|
+
|
|
628
|
+
function Widget({ color, cssColor, ...allProps }: WidgetProps) {
|
|
629
|
+
return (
|
|
630
|
+
<$ $el={<ThirdParty color={color} />} color={cssColor} {...allProps} />
|
|
631
|
+
);
|
|
632
|
+
}
|
|
633
|
+
```
|
|
634
|
+
|
|
635
|
+
---
|
|
636
|
+
|
|
637
|
+
## Conditional Styles
|
|
638
|
+
|
|
639
|
+
### Boolean Conditions
|
|
640
|
+
|
|
641
|
+
```tsx
|
|
642
|
+
<$.div
|
|
643
|
+
opacity={isDisabled ? 0.5 : 1}
|
|
644
|
+
pointer-events={isDisabled ? 'none' : 'auto'}
|
|
645
|
+
background={isActive ? 'blue' : 'gray'}
|
|
646
|
+
/>
|
|
647
|
+
```
|
|
648
|
+
|
|
649
|
+
### Conditional `$css` with Arrays
|
|
650
|
+
|
|
651
|
+
```tsx
|
|
652
|
+
<$.div
|
|
653
|
+
$css={[
|
|
654
|
+
{ padding: 16 },
|
|
655
|
+
isLarge && { padding: 24, fontSize: 18 },
|
|
656
|
+
hasError && { borderColor: 'red' },
|
|
657
|
+
isDisabled && { opacity: 0.5, pointerEvents: 'none' }
|
|
658
|
+
]}
|
|
659
|
+
/>
|
|
660
|
+
```
|
|
661
|
+
|
|
662
|
+
### State-Based Styles
|
|
663
|
+
|
|
664
|
+
```tsx
|
|
665
|
+
type ButtonState = 'idle' | 'loading' | 'success' | 'error';
|
|
666
|
+
|
|
667
|
+
function StatefulButton({ state, children }: { state: ButtonState; children: React.ReactNode }) {
|
|
668
|
+
const stateStyles = {
|
|
669
|
+
idle: { background: '#0066cc' },
|
|
670
|
+
loading: { background: '#666', cursor: 'wait' },
|
|
671
|
+
success: { background: '#00aa00' },
|
|
672
|
+
error: { background: '#cc0000' }
|
|
673
|
+
};
|
|
674
|
+
|
|
675
|
+
return (
|
|
676
|
+
<$.button
|
|
677
|
+
color="white"
|
|
678
|
+
padding="10px 20px"
|
|
679
|
+
border="none"
|
|
680
|
+
{...stateStyles[state]}
|
|
681
|
+
>
|
|
682
|
+
{children}
|
|
683
|
+
</$.button>
|
|
684
|
+
);
|
|
685
|
+
}
|
|
686
|
+
```
|
|
687
|
+
|
|
688
|
+
---
|
|
689
|
+
|
|
690
|
+
## Global Styles
|
|
691
|
+
|
|
692
|
+
> **Note:** Global styles are only applied while the component is mounted. Use `useGlobalStyles` when you need Stylix features like media keywords or plugins. For static styles that don't need these features, a plain CSS file avoids the mount/unmount behavior.
|
|
693
|
+
|
|
694
|
+
`useGlobalStyles` is useful for responsive global styles that leverage your configured breakpoints:
|
|
695
|
+
|
|
696
|
+
```tsx
|
|
697
|
+
import { useGlobalStyles } from '@stylix/core';
|
|
698
|
+
|
|
699
|
+
function ResponsiveTypography() {
|
|
700
|
+
useGlobalStyles({
|
|
701
|
+
h1: {
|
|
702
|
+
fontSize: { default: 32, tablet: 28, mobile: 24 },
|
|
703
|
+
marginBottom: { default: 24, mobile: 16 }
|
|
704
|
+
},
|
|
705
|
+
h2: {
|
|
706
|
+
fontSize: { default: 24, tablet: 22, mobile: 20 },
|
|
707
|
+
marginBottom: { default: 20, mobile: 12 }
|
|
708
|
+
},
|
|
709
|
+
body: {
|
|
710
|
+
fontSize: { default: 16, mobile: 14 },
|
|
711
|
+
lineHeight: 1.6
|
|
712
|
+
}
|
|
713
|
+
});
|
|
714
|
+
|
|
715
|
+
return null;
|
|
716
|
+
}
|
|
717
|
+
```
|
|
718
|
+
|
|
719
|
+
---
|
|
720
|
+
|
|
721
|
+
## Debugging Tips
|
|
722
|
+
|
|
723
|
+
### Inspecting Generated Styles
|
|
724
|
+
|
|
725
|
+
Stylix generates class names like `stylix-0`, `stylix-1`, etc. To see what styles are applied:
|
|
726
|
+
|
|
727
|
+
1. Inspect the element in browser DevTools
|
|
728
|
+
2. Look at the `<style>` tags in `<head>` (or inline for SSR)
|
|
729
|
+
3. Search for the class name to see the CSS rules
|
|
730
|
+
|
|
731
|
+
### Tracing Style Sources
|
|
732
|
+
|
|
733
|
+
When debugging unexpected styles, check:
|
|
734
|
+
|
|
735
|
+
1. **Direct props** on the element
|
|
736
|
+
2. **`$css` prop** for complex styles
|
|
737
|
+
3. **Spread props** (`{...styles}`) from parent components
|
|
738
|
+
4. **Media objects** that might apply at current viewport
|
|
739
|
+
5. **Global styles** that might match
|
|
740
|
+
|
|
741
|
+
### Common Issues
|
|
742
|
+
|
|
743
|
+
**Styles not applying:**
|
|
744
|
+
- Check if the prop name is correct (both `font-size` and `fontSize` work)
|
|
745
|
+
- Verify the value is valid CSS
|
|
746
|
+
- Check for typos in media keywords
|
|
747
|
+
|
|
748
|
+
**Styles being overridden:**
|
|
749
|
+
- In `$css` arrays, later items take precedence
|
|
750
|
+
- Check if parent components are spreading styles after your defaults
|
|
751
|
+
- Verify cascade layer ordering if using `@layer`
|
|
752
|
+
|
|
753
|
+
**Performance issues:**
|
|
754
|
+
- See the [Performance Guide](./performance.md)
|