0
# WithStyles HOC
1
2
The WithStyles Higher-Order Component (HOC) provides a pattern for injecting styles into React components. It supports both function and class components with full TypeScript integration and is compatible with Material-UI v4 withStyles patterns.
3
4
## Capabilities
5
6
### WithStyles Factory Function
7
8
Creates a withStyles HOC function with theme support and optional custom cache configuration.
9
10
```typescript { .api }
11
/**
12
* Creates a withStyles HOC function with theme support
13
* @param params - Configuration object with theme provider and optional cache
14
* @returns Object containing withStyles function
15
*/
16
function createWithStyles<Theme>(params: {
17
useTheme: () => Theme;
18
cache?: EmotionCache;
19
}): {
20
withStyles<
21
Component extends ReactComponent<any> | keyof ReactHTML,
22
Props extends ComponentProps<Component>,
23
CssObjectByRuleName extends Record<string, CSSObject>
24
>(
25
Component: Component,
26
cssObjectByRuleNameOrGetCssObjectByRuleName:
27
| CssObjectByRuleName
28
| ((theme: Theme, props: Props, classes: Record<string, string>) => CssObjectByRuleName)
29
): ComponentType<Props>;
30
};
31
32
type ReactComponent<P> = ComponentType<P>;
33
type ComponentProps<T> = T extends ComponentType<infer P> ? P : T extends keyof ReactHTML ? ReactHTML[T] extends ComponentType<infer P> ? P : never : never;
34
```
35
36
**Usage Examples:**
37
38
```typescript
39
import { useTheme } from "@mui/material/styles";
40
import { createWithStyles } from "tss-react";
41
42
// Create withStyles with MUI theme
43
const { withStyles } = createWithStyles({ useTheme });
44
45
// Custom cache configuration
46
import createCache from "@emotion/cache";
47
48
const customCache = createCache({
49
key: "my-styles",
50
prepend: true
51
});
52
53
const { withStyles: withStylesCustomCache } = createWithStyles({
54
useTheme,
55
cache: customCache
56
});
57
```
58
59
### WithStyles HOC Usage
60
61
Higher-order component that wraps components with style injection capabilities.
62
63
```typescript { .api }
64
/**
65
* Higher-order component for injecting styles into React components
66
* @param Component - React component to wrap (function, class, or HTML element)
67
* @param cssObjectByRuleNameOrGetCssObjectByRuleName - Static styles object or function
68
* @returns Enhanced component with injected classes prop
69
*/
70
function withStyles<
71
Component extends ReactComponent<any> | keyof ReactHTML,
72
Props extends ComponentProps<Component>,
73
CssObjectByRuleName extends Record<string, CSSObject>
74
>(
75
Component: Component,
76
cssObjectByRuleNameOrGetCssObjectByRuleName:
77
| CssObjectByRuleName
78
| ((
79
theme: Theme,
80
props: Props,
81
classes: Record<keyof CssObjectByRuleName, string>
82
) => CssObjectByRuleName)
83
): ComponentType<Props & { classes?: Partial<Record<keyof CssObjectByRuleName, string>> }>;
84
```
85
86
**Usage Examples:**
87
88
```typescript
89
import React from "react";
90
import { useTheme } from "@mui/material/styles";
91
import { createWithStyles } from "tss-react";
92
93
const { withStyles } = createWithStyles({ useTheme });
94
95
// Function component with static styles
96
const Button = withStyles(
97
({ children, classes, ...props }: {
98
children: React.ReactNode;
99
classes?: { root?: string; label?: string };
100
}) => (
101
<button className={classes?.root} {...props}>
102
<span className={classes?.label}>{children}</span>
103
</button>
104
),
105
{
106
root: {
107
backgroundColor: "blue",
108
color: "white",
109
border: "none",
110
borderRadius: 4,
111
padding: "8px 16px",
112
cursor: "pointer",
113
"&:hover": {
114
backgroundColor: "darkblue"
115
}
116
},
117
label: {
118
fontWeight: "bold"
119
}
120
}
121
);
122
123
// Function component with dynamic styles
124
interface CardProps {
125
title: string;
126
elevated: boolean;
127
classes?: { root?: string; title?: string; content?: string };
128
children: React.ReactNode;
129
}
130
131
const Card = withStyles(
132
({ title, children, classes, elevated, ...props }: CardProps) => (
133
<div className={classes?.root} {...props}>
134
<h3 className={classes?.title}>{title}</h3>
135
<div className={classes?.content}>{children}</div>
136
</div>
137
),
138
(theme, { elevated }) => ({
139
root: {
140
backgroundColor: theme.palette.background.paper,
141
borderRadius: theme.shape.borderRadius,
142
padding: theme.spacing(2),
143
boxShadow: elevated ? theme.shadows[4] : theme.shadows[1],
144
transition: theme.transitions.create("box-shadow")
145
},
146
title: {
147
margin: 0,
148
marginBottom: theme.spacing(1),
149
color: theme.palette.text.primary,
150
fontSize: theme.typography.h6.fontSize
151
},
152
content: {
153
color: theme.palette.text.secondary
154
}
155
})
156
);
157
158
// HTML element enhancement
159
const StyledDiv = withStyles(
160
"div",
161
theme => ({
162
root: {
163
backgroundColor: theme.palette.background.default,
164
padding: theme.spacing(3),
165
minHeight: "100vh"
166
}
167
})
168
);
169
170
// Usage
171
function App() {
172
return (
173
<StyledDiv>
174
<Card title="Welcome" elevated={true}>
175
<p>This is a styled card component.</p>
176
<Button>Click me</Button>
177
</Card>
178
</StyledDiv>
179
);
180
}
181
```
182
183
### Class Component Support
184
185
WithStyles works seamlessly with React class components:
186
187
```typescript
188
import React, { Component } from "react";
189
190
interface MyClassComponentProps {
191
title: string;
192
classes?: {
193
root?: string;
194
title?: string;
195
button?: string;
196
};
197
}
198
199
interface MyClassComponentState {
200
count: number;
201
}
202
203
class MyClassComponent extends Component<MyClassComponentProps, MyClassComponentState> {
204
state = { count: 0 };
205
206
handleClick = () => {
207
this.setState(prev => ({ count: prev.count + 1 }));
208
};
209
210
render() {
211
const { title, classes } = this.props;
212
const { count } = this.state;
213
214
return (
215
<div className={classes?.root}>
216
<h2 className={classes?.title}>{title}</h2>
217
<p>Count: {count}</p>
218
<button className={classes?.button} onClick={this.handleClick}>
219
Increment
220
</button>
221
</div>
222
);
223
}
224
}
225
226
const StyledClassComponent = withStyles(
227
MyClassComponent,
228
theme => ({
229
root: {
230
padding: theme.spacing(2),
231
backgroundColor: theme.palette.background.paper,
232
borderRadius: theme.shape.borderRadius
233
},
234
title: {
235
color: theme.palette.primary.main,
236
marginBottom: theme.spacing(1)
237
},
238
button: {
239
backgroundColor: theme.palette.secondary.main,
240
color: theme.palette.secondary.contrastText,
241
border: "none",
242
padding: theme.spacing(1, 2),
243
borderRadius: theme.shape.borderRadius,
244
cursor: "pointer"
245
}
246
})
247
);
248
```
249
250
### Style Overrides
251
252
Components wrapped with withStyles accept a `classes` prop for style customization:
253
254
```typescript
255
function CustomizedCard() {
256
return (
257
<Card
258
title="Custom Card"
259
elevated={false}
260
classes={{
261
root: "my-custom-root-class",
262
title: "my-custom-title-class"
263
}}
264
>
265
<p>This card has custom styling applied.</p>
266
</Card>
267
);
268
}
269
270
// CSS-in-JS style overrides
271
const useOverrideStyles = makeStyles()(theme => ({
272
customRoot: {
273
backgroundColor: theme.palette.warning.light,
274
border: `2px solid ${theme.palette.warning.main}`
275
},
276
customTitle: {
277
color: theme.palette.warning.contrastText,
278
textTransform: "uppercase"
279
}
280
}));
281
282
function CssInJsOverrides() {
283
const { classes } = useOverrideStyles();
284
285
return (
286
<Card
287
title="CSS-in-JS Overrides"
288
elevated={false}
289
classes={{
290
root: classes.customRoot,
291
title: classes.customTitle
292
}}
293
>
294
<p>Styled with CSS-in-JS overrides.</p>
295
</Card>
296
);
297
}
298
```
299
300
### Migration from Material-UI v4
301
302
The withStyles API provides seamless migration from @material-ui/core v4:
303
304
```typescript
305
// Before (Material-UI v4)
306
import { withStyles } from "@material-ui/core/styles";
307
308
const StyledComponent = withStyles(theme => ({
309
root: {
310
backgroundColor: theme.palette.background.paper,
311
padding: theme.spacing(2)
312
}
313
}))(({ classes }) => (
314
<div className={classes.root}>Content</div>
315
));
316
317
// After (TSS-React)
318
import { createWithStyles } from "tss-react";
319
import { useTheme } from "@mui/material/styles";
320
321
const { withStyles } = createWithStyles({ useTheme });
322
323
const StyledComponent = withStyles(
324
({ classes }: { classes?: { root?: string } }) => (
325
<div className={classes?.root}>Content</div>
326
),
327
theme => ({
328
root: {
329
backgroundColor: theme.palette.background.paper,
330
padding: theme.spacing(2)
331
}
332
})
333
);
334
```
335
336
### Advanced Patterns
337
338
#### Nested Selectors with Classes Reference
339
340
```typescript
341
const NestedComponent = withStyles(
342
({ classes }: { classes?: { root?: string; item?: string; selected?: string } }) => (
343
<div className={classes?.root}>
344
<div className={classes?.item}>Item 1</div>
345
<div className={`${classes?.item} ${classes?.selected}`}>Item 2 (Selected)</div>
346
</div>
347
),
348
(theme, props, classes) => ({
349
root: {
350
padding: theme.spacing(2),
351
[`& .${classes.item}`]: {
352
padding: theme.spacing(1),
353
borderBottom: `1px solid ${theme.palette.divider}`,
354
"&:last-child": {
355
borderBottom: "none"
356
}
357
},
358
[`& .${classes.selected}`]: {
359
backgroundColor: theme.palette.action.selected,
360
fontWeight: theme.typography.fontWeightBold
361
}
362
},
363
item: {},
364
selected: {}
365
})
366
);
367
```
368
369
#### Conditional Styling with Props
370
371
```typescript
372
interface AlertProps {
373
severity: "info" | "warning" | "error" | "success";
374
message: string;
375
classes?: { root?: string; icon?: string; message?: string };
376
}
377
378
const Alert = withStyles(
379
({ severity, message, classes }: AlertProps) => (
380
<div className={classes?.root}>
381
<span className={classes?.icon}>⚠️</span>
382
<span className={classes?.message}>{message}</span>
383
</div>
384
),
385
(theme, { severity }) => {
386
const colors = {
387
info: theme.palette.info,
388
warning: theme.palette.warning,
389
error: theme.palette.error,
390
success: theme.palette.success
391
};
392
393
const color = colors[severity];
394
395
return {
396
root: {
397
display: "flex",
398
alignItems: "center",
399
padding: theme.spacing(1, 2),
400
backgroundColor: color.light,
401
color: color.contrastText,
402
borderRadius: theme.shape.borderRadius,
403
border: `1px solid ${color.main}`
404
},
405
icon: {
406
marginRight: theme.spacing(1),
407
fontSize: "1.2em"
408
},
409
message: {
410
flex: 1
411
}
412
};
413
}
414
);
415
```
416
417
### TypeScript Integration
418
419
WithStyles provides full TypeScript support with proper prop inference:
420
421
```typescript
422
// Component with strict typing
423
interface StrictButtonProps {
424
variant: "primary" | "secondary";
425
size: "small" | "medium" | "large";
426
disabled?: boolean;
427
children: React.ReactNode;
428
onClick?: () => void;
429
classes?: {
430
root?: string;
431
label?: string;
432
};
433
}
434
435
const StrictButton = withStyles(
436
({ variant, size, disabled, children, classes, onClick }: StrictButtonProps) => (
437
<button
438
className={classes?.root}
439
disabled={disabled}
440
onClick={onClick}
441
>
442
<span className={classes?.label}>{children}</span>
443
</button>
444
),
445
(theme, { variant, size, disabled }) => ({
446
root: {
447
backgroundColor: variant === "primary" ? theme.palette.primary.main : theme.palette.secondary.main,
448
color: variant === "primary" ? theme.palette.primary.contrastText : theme.palette.secondary.contrastText,
449
padding: {
450
small: theme.spacing(0.5, 1),
451
medium: theme.spacing(1, 2),
452
large: theme.spacing(1.5, 3)
453
}[size],
454
fontSize: {
455
small: theme.typography.body2.fontSize,
456
medium: theme.typography.body1.fontSize,
457
large: theme.typography.h6.fontSize
458
}[size],
459
opacity: disabled ? 0.5 : 1,
460
cursor: disabled ? "not-allowed" : "pointer",
461
border: "none",
462
borderRadius: theme.shape.borderRadius,
463
transition: theme.transitions.create(["background-color", "opacity"])
464
},
465
label: {
466
fontWeight: theme.typography.fontWeightMedium
467
}
468
})
469
);
470
471
// Usage with full type checking
472
function TypedExample() {
473
return (
474
<StrictButton
475
variant="primary"
476
size="medium"
477
onClick={() => console.log("Clicked!")}
478
>
479
Click me
480
</StrictButton>
481
);
482
}
483
```
484
485
### GetClasses Utility
486
487
The withStyles HOC includes a `getClasses` utility function for accessing generated class names within component render functions. This is particularly useful for programmatic access to styles.
488
489
```typescript { .api }
490
/**
491
* Utility function attached to withStyles for accessing class names
492
* @param props - Component props containing className and classes
493
* @returns Generated class names object
494
*/
495
withStyles.getClasses = function getClasses<Classes>(props: {
496
className?: string;
497
classes?: Classes;
498
}): Classes extends Record<string, unknown>
499
? Classes extends Partial<Record<infer K, any>>
500
? Record<K, string>
501
: Classes
502
: { root: string };
503
```
504
505
**Usage Examples:**
506
507
```typescript
508
import { createWithStyles } from "tss-react";
509
import { useTheme } from "@mui/material/styles";
510
511
const { withStyles } = createWithStyles({ useTheme });
512
513
// Component that uses getClasses utility
514
interface CardProps {
515
title: string;
516
content: string;
517
className?: string;
518
classes?: {
519
root?: string;
520
header?: string;
521
title?: string;
522
content?: string;
523
footer?: string;
524
};
525
}
526
527
const Card = withStyles(
528
(props: CardProps) => {
529
// Access classes programmatically
530
const classes = withStyles.getClasses(props);
531
532
return (
533
<div className={classes.root}>
534
<header className={classes.header}>
535
<h2 className={classes.title}>{props.title}</h2>
536
</header>
537
<div className={classes.content}>
538
{props.content}
539
</div>
540
<footer className={classes.footer}>
541
<button>Action</button>
542
</footer>
543
</div>
544
);
545
},
546
theme => ({
547
root: {
548
backgroundColor: theme.palette.background.paper,
549
borderRadius: theme.shape.borderRadius,
550
boxShadow: theme.shadows[2],
551
overflow: "hidden"
552
},
553
header: {
554
backgroundColor: theme.palette.primary.main,
555
color: theme.palette.primary.contrastText,
556
padding: theme.spacing(2)
557
},
558
title: {
559
margin: 0,
560
fontSize: theme.typography.h5.fontSize,
561
fontWeight: theme.typography.fontWeightMedium
562
},
563
content: {
564
padding: theme.spacing(2),
565
color: theme.palette.text.primary
566
},
567
footer: {
568
padding: theme.spacing(1, 2),
569
borderTop: `1px solid ${theme.palette.divider}`,
570
backgroundColor: theme.palette.background.default
571
}
572
})
573
);
574
575
// Usage
576
function App() {
577
return (
578
<Card
579
title="My Card"
580
content="This is the card content"
581
classes={{
582
root: "custom-card-root",
583
title: "custom-card-title"
584
}}
585
/>
586
);
587
}
588
```
589
590
**Important Notes:**
591
592
- `getClasses` should only be used within components wrapped by withStyles
593
- The function expects props to contain a `classes` object provided by withStyles
594
- It returns the actual generated CSS class names for programmatic use
595
- Useful for complex conditional styling logic that needs access to class names
596
- The returned classes object maintains type safety based on the component's classes interface
597
598
**Error Handling:**
599
600
```typescript
601
// getClasses will throw an error if used incorrectly
602
try {
603
const classes = withStyles.getClasses({ classes: undefined });
604
} catch (error) {
605
console.error("getClasses should only be used in conjunction with withStyles");
606
}
607
```