Type safe CSS-in-JS API heavily inspired by react-jss
—
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.
Creates a withStyles HOC function with theme support and optional custom cache configuration.
/**
* Creates a withStyles HOC function with theme support
* @param params - Configuration object with theme provider and optional cache
* @returns Object containing withStyles function
*/
function createWithStyles<Theme>(params: {
useTheme: () => Theme;
cache?: EmotionCache;
}): {
withStyles<
Component extends ReactComponent<any> | keyof ReactHTML,
Props extends ComponentProps<Component>,
CssObjectByRuleName extends Record<string, CSSObject>
>(
Component: Component,
cssObjectByRuleNameOrGetCssObjectByRuleName:
| CssObjectByRuleName
| ((theme: Theme, props: Props, classes: Record<string, string>) => CssObjectByRuleName)
): ComponentType<Props>;
};
type ReactComponent<P> = ComponentType<P>;
type ComponentProps<T> = T extends ComponentType<infer P> ? P : T extends keyof ReactHTML ? ReactHTML[T] extends ComponentType<infer P> ? P : never : never;Usage Examples:
import { useTheme } from "@mui/material/styles";
import { createWithStyles } from "tss-react";
// Create withStyles with MUI theme
const { withStyles } = createWithStyles({ useTheme });
// Custom cache configuration
import createCache from "@emotion/cache";
const customCache = createCache({
key: "my-styles",
prepend: true
});
const { withStyles: withStylesCustomCache } = createWithStyles({
useTheme,
cache: customCache
});Higher-order component that wraps components with style injection capabilities.
/**
* Higher-order component for injecting styles into React components
* @param Component - React component to wrap (function, class, or HTML element)
* @param cssObjectByRuleNameOrGetCssObjectByRuleName - Static styles object or function
* @returns Enhanced component with injected classes prop
*/
function withStyles<
Component extends ReactComponent<any> | keyof ReactHTML,
Props extends ComponentProps<Component>,
CssObjectByRuleName extends Record<string, CSSObject>
>(
Component: Component,
cssObjectByRuleNameOrGetCssObjectByRuleName:
| CssObjectByRuleName
| ((
theme: Theme,
props: Props,
classes: Record<keyof CssObjectByRuleName, string>
) => CssObjectByRuleName)
): ComponentType<Props & { classes?: Partial<Record<keyof CssObjectByRuleName, string>> }>;Usage Examples:
import React from "react";
import { useTheme } from "@mui/material/styles";
import { createWithStyles } from "tss-react";
const { withStyles } = createWithStyles({ useTheme });
// Function component with static styles
const Button = withStyles(
({ children, classes, ...props }: {
children: React.ReactNode;
classes?: { root?: string; label?: string };
}) => (
<button className={classes?.root} {...props}>
<span className={classes?.label}>{children}</span>
</button>
),
{
root: {
backgroundColor: "blue",
color: "white",
border: "none",
borderRadius: 4,
padding: "8px 16px",
cursor: "pointer",
"&:hover": {
backgroundColor: "darkblue"
}
},
label: {
fontWeight: "bold"
}
}
);
// Function component with dynamic styles
interface CardProps {
title: string;
elevated: boolean;
classes?: { root?: string; title?: string; content?: string };
children: React.ReactNode;
}
const Card = withStyles(
({ title, children, classes, elevated, ...props }: CardProps) => (
<div className={classes?.root} {...props}>
<h3 className={classes?.title}>{title}</h3>
<div className={classes?.content}>{children}</div>
</div>
),
(theme, { elevated }) => ({
root: {
backgroundColor: theme.palette.background.paper,
borderRadius: theme.shape.borderRadius,
padding: theme.spacing(2),
boxShadow: elevated ? theme.shadows[4] : theme.shadows[1],
transition: theme.transitions.create("box-shadow")
},
title: {
margin: 0,
marginBottom: theme.spacing(1),
color: theme.palette.text.primary,
fontSize: theme.typography.h6.fontSize
},
content: {
color: theme.palette.text.secondary
}
})
);
// HTML element enhancement
const StyledDiv = withStyles(
"div",
theme => ({
root: {
backgroundColor: theme.palette.background.default,
padding: theme.spacing(3),
minHeight: "100vh"
}
})
);
// Usage
function App() {
return (
<StyledDiv>
<Card title="Welcome" elevated={true}>
<p>This is a styled card component.</p>
<Button>Click me</Button>
</Card>
</StyledDiv>
);
}WithStyles works seamlessly with React class components:
import React, { Component } from "react";
interface MyClassComponentProps {
title: string;
classes?: {
root?: string;
title?: string;
button?: string;
};
}
interface MyClassComponentState {
count: number;
}
class MyClassComponent extends Component<MyClassComponentProps, MyClassComponentState> {
state = { count: 0 };
handleClick = () => {
this.setState(prev => ({ count: prev.count + 1 }));
};
render() {
const { title, classes } = this.props;
const { count } = this.state;
return (
<div className={classes?.root}>
<h2 className={classes?.title}>{title}</h2>
<p>Count: {count}</p>
<button className={classes?.button} onClick={this.handleClick}>
Increment
</button>
</div>
);
}
}
const StyledClassComponent = withStyles(
MyClassComponent,
theme => ({
root: {
padding: theme.spacing(2),
backgroundColor: theme.palette.background.paper,
borderRadius: theme.shape.borderRadius
},
title: {
color: theme.palette.primary.main,
marginBottom: theme.spacing(1)
},
button: {
backgroundColor: theme.palette.secondary.main,
color: theme.palette.secondary.contrastText,
border: "none",
padding: theme.spacing(1, 2),
borderRadius: theme.shape.borderRadius,
cursor: "pointer"
}
})
);Components wrapped with withStyles accept a classes prop for style customization:
function CustomizedCard() {
return (
<Card
title="Custom Card"
elevated={false}
classes={{
root: "my-custom-root-class",
title: "my-custom-title-class"
}}
>
<p>This card has custom styling applied.</p>
</Card>
);
}
// CSS-in-JS style overrides
const useOverrideStyles = makeStyles()(theme => ({
customRoot: {
backgroundColor: theme.palette.warning.light,
border: `2px solid ${theme.palette.warning.main}`
},
customTitle: {
color: theme.palette.warning.contrastText,
textTransform: "uppercase"
}
}));
function CssInJsOverrides() {
const { classes } = useOverrideStyles();
return (
<Card
title="CSS-in-JS Overrides"
elevated={false}
classes={{
root: classes.customRoot,
title: classes.customTitle
}}
>
<p>Styled with CSS-in-JS overrides.</p>
</Card>
);
}The withStyles API provides seamless migration from @material-ui/core v4:
// Before (Material-UI v4)
import { withStyles } from "@material-ui/core/styles";
const StyledComponent = withStyles(theme => ({
root: {
backgroundColor: theme.palette.background.paper,
padding: theme.spacing(2)
}
}))(({ classes }) => (
<div className={classes.root}>Content</div>
));
// After (TSS-React)
import { createWithStyles } from "tss-react";
import { useTheme } from "@mui/material/styles";
const { withStyles } = createWithStyles({ useTheme });
const StyledComponent = withStyles(
({ classes }: { classes?: { root?: string } }) => (
<div className={classes?.root}>Content</div>
),
theme => ({
root: {
backgroundColor: theme.palette.background.paper,
padding: theme.spacing(2)
}
})
);const NestedComponent = withStyles(
({ classes }: { classes?: { root?: string; item?: string; selected?: string } }) => (
<div className={classes?.root}>
<div className={classes?.item}>Item 1</div>
<div className={`${classes?.item} ${classes?.selected}`}>Item 2 (Selected)</div>
</div>
),
(theme, props, classes) => ({
root: {
padding: theme.spacing(2),
[`& .${classes.item}`]: {
padding: theme.spacing(1),
borderBottom: `1px solid ${theme.palette.divider}`,
"&:last-child": {
borderBottom: "none"
}
},
[`& .${classes.selected}`]: {
backgroundColor: theme.palette.action.selected,
fontWeight: theme.typography.fontWeightBold
}
},
item: {},
selected: {}
})
);interface AlertProps {
severity: "info" | "warning" | "error" | "success";
message: string;
classes?: { root?: string; icon?: string; message?: string };
}
const Alert = withStyles(
({ severity, message, classes }: AlertProps) => (
<div className={classes?.root}>
<span className={classes?.icon}>⚠️</span>
<span className={classes?.message}>{message}</span>
</div>
),
(theme, { severity }) => {
const colors = {
info: theme.palette.info,
warning: theme.palette.warning,
error: theme.palette.error,
success: theme.palette.success
};
const color = colors[severity];
return {
root: {
display: "flex",
alignItems: "center",
padding: theme.spacing(1, 2),
backgroundColor: color.light,
color: color.contrastText,
borderRadius: theme.shape.borderRadius,
border: `1px solid ${color.main}`
},
icon: {
marginRight: theme.spacing(1),
fontSize: "1.2em"
},
message: {
flex: 1
}
};
}
);WithStyles provides full TypeScript support with proper prop inference:
// Component with strict typing
interface StrictButtonProps {
variant: "primary" | "secondary";
size: "small" | "medium" | "large";
disabled?: boolean;
children: React.ReactNode;
onClick?: () => void;
classes?: {
root?: string;
label?: string;
};
}
const StrictButton = withStyles(
({ variant, size, disabled, children, classes, onClick }: StrictButtonProps) => (
<button
className={classes?.root}
disabled={disabled}
onClick={onClick}
>
<span className={classes?.label}>{children}</span>
</button>
),
(theme, { variant, size, disabled }) => ({
root: {
backgroundColor: variant === "primary" ? theme.palette.primary.main : theme.palette.secondary.main,
color: variant === "primary" ? theme.palette.primary.contrastText : theme.palette.secondary.contrastText,
padding: {
small: theme.spacing(0.5, 1),
medium: theme.spacing(1, 2),
large: theme.spacing(1.5, 3)
}[size],
fontSize: {
small: theme.typography.body2.fontSize,
medium: theme.typography.body1.fontSize,
large: theme.typography.h6.fontSize
}[size],
opacity: disabled ? 0.5 : 1,
cursor: disabled ? "not-allowed" : "pointer",
border: "none",
borderRadius: theme.shape.borderRadius,
transition: theme.transitions.create(["background-color", "opacity"])
},
label: {
fontWeight: theme.typography.fontWeightMedium
}
})
);
// Usage with full type checking
function TypedExample() {
return (
<StrictButton
variant="primary"
size="medium"
onClick={() => console.log("Clicked!")}
>
Click me
</StrictButton>
);
}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.
/**
* Utility function attached to withStyles for accessing class names
* @param props - Component props containing className and classes
* @returns Generated class names object
*/
withStyles.getClasses = function getClasses<Classes>(props: {
className?: string;
classes?: Classes;
}): Classes extends Record<string, unknown>
? Classes extends Partial<Record<infer K, any>>
? Record<K, string>
: Classes
: { root: string };Usage Examples:
import { createWithStyles } from "tss-react";
import { useTheme } from "@mui/material/styles";
const { withStyles } = createWithStyles({ useTheme });
// Component that uses getClasses utility
interface CardProps {
title: string;
content: string;
className?: string;
classes?: {
root?: string;
header?: string;
title?: string;
content?: string;
footer?: string;
};
}
const Card = withStyles(
(props: CardProps) => {
// Access classes programmatically
const classes = withStyles.getClasses(props);
return (
<div className={classes.root}>
<header className={classes.header}>
<h2 className={classes.title}>{props.title}</h2>
</header>
<div className={classes.content}>
{props.content}
</div>
<footer className={classes.footer}>
<button>Action</button>
</footer>
</div>
);
},
theme => ({
root: {
backgroundColor: theme.palette.background.paper,
borderRadius: theme.shape.borderRadius,
boxShadow: theme.shadows[2],
overflow: "hidden"
},
header: {
backgroundColor: theme.palette.primary.main,
color: theme.palette.primary.contrastText,
padding: theme.spacing(2)
},
title: {
margin: 0,
fontSize: theme.typography.h5.fontSize,
fontWeight: theme.typography.fontWeightMedium
},
content: {
padding: theme.spacing(2),
color: theme.palette.text.primary
},
footer: {
padding: theme.spacing(1, 2),
borderTop: `1px solid ${theme.palette.divider}`,
backgroundColor: theme.palette.background.default
}
})
);
// Usage
function App() {
return (
<Card
title="My Card"
content="This is the card content"
classes={{
root: "custom-card-root",
title: "custom-card-title"
}}
/>
);
}Important Notes:
getClasses should only be used within components wrapped by withStylesclasses object provided by withStylesError Handling:
// getClasses will throw an error if used incorrectly
try {
const classes = withStyles.getClasses({ classes: undefined });
} catch (error) {
console.error("getClasses should only be used in conjunction with withStyles");
}Install with Tessl CLI
npx tessl i tessl/npm-tss-react