@stylix/core 6.1.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.
@@ -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)