Type safe CSS-in-JS API heavily inspired by react-jss
—
TSS-React provides utilities for global CSS injection and animation keyframe definitions, built on Emotion's proven CSS-in-JS infrastructure. These tools enable application-wide styling and complex animations.
React component for injecting global CSS styles into the document head.
/**
* React component for injecting global CSS styles
* @param props - Props containing the global styles definition
* @returns JSX element that injects global styles
*/
function GlobalStyles(props: { styles: CSSInterpolation }): JSX.Element;
type CSSInterpolation =
| CSSObject
| string
| number
| false
| null
| undefined
| CSSInterpolation[];
interface CSSObject {
[property: string]: CSSInterpolation;
label?: string;
}Usage Examples:
import { GlobalStyles } from "tss-react";
// Basic global styles
function App() {
return (
<>
<GlobalStyles
styles={{
body: {
margin: 0,
padding: 0,
fontFamily: '"Helvetica Neue", Arial, sans-serif',
backgroundColor: "#f5f5f5",
color: "#333"
},
"*": {
boxSizing: "border-box"
},
"*, *::before, *::after": {
boxSizing: "border-box"
}
}}
/>
<MyAppContent />
</>
);
}
// Theme-based global styles
function ThemedApp({ theme }: { theme: any }) {
return (
<>
<GlobalStyles
styles={{
body: {
backgroundColor: theme.palette.background.default,
color: theme.palette.text.primary,
fontFamily: theme.typography.fontFamily,
fontSize: theme.typography.body1.fontSize,
lineHeight: theme.typography.body1.lineHeight
},
a: {
color: theme.palette.primary.main,
textDecoration: "none",
"&:hover": {
textDecoration: "underline"
}
},
"h1, h2, h3, h4, h5, h6": {
fontWeight: theme.typography.fontWeightBold,
margin: "0 0 16px 0"
}
}}
/>
<AppContent />
</>
);
}
// CSS Reset with GlobalStyles
function CSSReset() {
return (
<GlobalStyles
styles={{
// Modern CSS reset
"*, *::before, *::after": {
boxSizing: "border-box"
},
"*": {
margin: 0
},
"html, body": {
height: "100%"
},
body: {
lineHeight: 1.5,
"-webkit-font-smoothing": "antialiased"
},
"img, picture, video, canvas, svg": {
display: "block",
maxWidth: "100%"
},
"input, button, textarea, select": {
font: "inherit"
},
"p, h1, h2, h3, h4, h5, h6": {
overflowWrap: "break-word"
},
"#root, #__next": {
isolation: "isolate"
}
}}
/>
);
}
// Multiple GlobalStyles components
function MultiGlobalStyles() {
return (
<>
{/* Base reset */}
<GlobalStyles
styles={{
"*": { boxSizing: "border-box" },
body: { margin: 0, fontFamily: "system-ui" }
}}
/>
{/* Custom scrollbar */}
<GlobalStyles
styles={{
"::-webkit-scrollbar": {
width: 8
},
"::-webkit-scrollbar-track": {
backgroundColor: "#f1f1f1"
},
"::-webkit-scrollbar-thumb": {
backgroundColor: "#888",
borderRadius: 4,
"&:hover": {
backgroundColor: "#555"
}
}
}}
/>
{/* Print styles */}
<GlobalStyles
styles={{
"@media print": {
"*": {
color: "black !important",
backgroundColor: "white !important"
},
".no-print": {
display: "none !important"
}
}
}}
/>
<AppContent />
</>
);
}Function for defining CSS animation keyframes, re-exported from @emotion/react.
/**
* Creates CSS animation keyframes (template literal version)
* @param template - Template strings array containing keyframe definitions
* @param args - Interpolated values for keyframes
* @returns Animation name string for use in CSS animations
*/
function keyframes(
template: TemplateStringsArray,
...args: CSSInterpolation[]
): string;
/**
* Creates CSS animation keyframes (function version)
* @param args - CSS interpolation values containing keyframe definitions
* @returns Animation name string for use in CSS animations
*/
function keyframes(...args: CSSInterpolation[]): string;Usage Examples:
import { keyframes, tss } from "tss-react";
// Template literal keyframes
const fadeIn = keyframes`
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
`;
const slideIn = keyframes`
0% {
transform: translateX(-100%);
opacity: 0;
}
50% {
opacity: 0.5;
}
100% {
transform: translateX(0);
opacity: 1;
}
`;
// Object-based keyframes
const bounce = keyframes({
"0%, 20%, 53%, 80%, 100%": {
transform: "translate3d(0, 0, 0)"
},
"40%, 43%": {
transform: "translate3d(0, -30px, 0)"
},
"70%": {
transform: "translate3d(0, -15px, 0)"
},
"90%": {
transform: "translate3d(0, -4px, 0)"
}
});
const pulse = keyframes({
"0%": {
transform: "scale(1)",
opacity: 1
},
"50%": {
transform: "scale(1.05)",
opacity: 0.8
},
"100%": {
transform: "scale(1)",
opacity: 1
}
});
// Using keyframes in TSS styles
const useAnimatedStyles = tss
.withParams<{
isVisible: boolean;
animationType: "fade" | "slide" | "bounce";
}>()
.create(({}, { isVisible, animationType }) => {
const animations = {
fade: fadeIn,
slide: slideIn,
bounce: bounce
};
return {
root: {
animation: isVisible
? `${animations[animationType]} 0.5s ease-out forwards`
: "none",
opacity: isVisible ? 1 : 0
},
pulsingButton: {
animation: `${pulse} 2s infinite`,
cursor: "pointer",
border: "none",
borderRadius: 4,
padding: "12px 24px",
backgroundColor: "#007bff",
color: "white",
fontSize: 16
}
};
});
function AnimatedComponent({
isVisible,
animationType
}: {
isVisible: boolean;
animationType: "fade" | "slide" | "bounce";
}) {
const { classes } = useAnimatedStyles({ isVisible, animationType });
return (
<div className={classes.root}>
<h2>Animated Content</h2>
<p>This content animates based on the selected animation type.</p>
<button className={classes.pulsingButton}>
Pulsing Button
</button>
</div>
);
}import { keyframes, tss } from "tss-react";
// Spinner keyframes
const spin = keyframes({
"0%": { transform: "rotate(0deg)" },
"100%": { transform: "rotate(360deg)" }
});
const dots = keyframes({
"0%, 80%, 100%": {
transform: "scale(0)",
opacity: 0.5
},
"40%": {
transform: "scale(1)",
opacity: 1
}
});
const shimmer = keyframes({
"0%": {
backgroundPosition: "-200px 0"
},
"100%": {
backgroundPosition: "calc(200px + 100%) 0"
}
});
const useLoadingStyles = tss.create({
spinner: {
width: 40,
height: 40,
border: "4px solid #f3f3f3",
borderTop: "4px solid #3498db",
borderRadius: "50%",
animation: `${spin} 1s linear infinite`
},
dotsLoader: {
display: "inline-block",
position: "relative",
width: 64,
height: 64,
"& div": {
position: "absolute",
top: 27,
width: 11,
height: 11,
borderRadius: "50%",
backgroundColor: "#3498db",
animationTimingFunction: "cubic-bezier(0, 1, 1, 0)"
},
"& div:nth-child(1)": {
left: 6,
animation: `${dots} 0.6s infinite`
},
"& div:nth-child(2)": {
left: 6,
animation: `${dots} 0.6s infinite`,
animationDelay: "-0.2s"
},
"& div:nth-child(3)": {
left: 26,
animation: `${dots} 0.6s infinite`,
animationDelay: "-0.4s"
}
},
shimmerCard: {
background: "#f6f7f8",
backgroundImage: `linear-gradient(
90deg,
#f6f7f8 0px,
rgba(255, 255, 255, 0.8) 40px,
#f6f7f8 80px
)`,
backgroundSize: "200px 100%",
backgroundRepeat: "no-repeat",
borderRadius: 4,
display: "inline-block",
lineHeight: 1,
width: "100%",
animation: `${shimmer} 1.2s ease-in-out infinite`
}
});
function LoadingComponents() {
const { classes } = useLoadingStyles();
return (
<div>
<div className={classes.spinner} />
<div className={classes.dotsLoader}>
<div></div>
<div></div>
<div></div>
</div>
<div className={classes.shimmerCard} style={{ height: 200 }} />
</div>
);
}import { keyframes, tss } from "tss-react";
// Interactive animation keyframes
const wiggle = keyframes({
"0%, 7%": { transform: "rotateZ(0)" },
"15%": { transform: "rotateZ(-15deg)" },
"20%": { transform: "rotateZ(10deg)" },
"25%": { transform: "rotateZ(-10deg)" },
"30%": { transform: "rotateZ(6deg)" },
"35%": { transform: "rotateZ(-4deg)" },
"40%, 100%": { transform: "rotateZ(0)" }
});
const heartbeat = keyframes({
"0%": { transform: "scale(1)" },
"14%": { transform: "scale(1.3)" },
"28%": { transform: "scale(1)" },
"42%": { transform: "scale(1.3)" },
"70%": { transform: "scale(1)" }
});
const rubber = keyframes({
"0%": { transform: "scale3d(1, 1, 1)" },
"30%": { transform: "scale3d(1.25, 0.75, 1)" },
"40%": { transform: "scale3d(0.75, 1.25, 1)" },
"50%": { transform: "scale3d(1.15, 0.85, 1)" },
"65%": { transform: "scale3d(0.95, 1.05, 1)" },
"75%": { transform: "scale3d(1.05, 0.95, 1)" },
"100%": { transform: "scale3d(1, 1, 1)" }
});
const useInteractiveStyles = tss
.withParams<{
isHovered: boolean;
isClicked: boolean;
animationStyle: "wiggle" | "heartbeat" | "rubber";
}>()
.create(({}, { isHovered, isClicked, animationStyle }) => {
const animations = {
wiggle: wiggle,
heartbeat: heartbeat,
rubber: rubber
};
const durations = {
wiggle: "0.5s",
heartbeat: "1.2s",
rubber: "1s"
};
return {
interactiveButton: {
padding: "12px 24px",
backgroundColor: "#28a745",
color: "white",
border: "none",
borderRadius: 8,
cursor: "pointer",
fontSize: 16,
fontWeight: 600,
transition: "all 0.2s ease",
transform: isClicked ? "scale(0.95)" : "scale(1)",
animation: isHovered
? `${animations[animationStyle]} ${durations[animationStyle]} ease-in-out`
: "none",
"&:hover": {
backgroundColor: "#218838",
boxShadow: "0 4px 8px rgba(0,0,0,0.2)"
}
}
};
});
function InteractiveButton({
animationStyle = "wiggle",
children,
onClick
}: {
animationStyle?: "wiggle" | "heartbeat" | "rubber";
children: React.ReactNode;
onClick?: () => void;
}) {
const [isHovered, setIsHovered] = useState(false);
const [isClicked, setIsClicked] = useState(false);
const { classes } = useInteractiveStyles({
isHovered,
isClicked,
animationStyle
});
const handleClick = () => {
setIsClicked(true);
setTimeout(() => setIsClicked(false), 150);
onClick?.();
};
return (
<button
className={classes.interactiveButton}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
onClick={handleClick}
>
{children}
</button>
);
}import { GlobalStyles } from "tss-react";
function CSSVariablesSetup({ theme }: { theme: any }) {
return (
<GlobalStyles
styles={{
":root": {
// Color palette
"--color-primary": theme.palette.primary.main,
"--color-primary-dark": theme.palette.primary.dark,
"--color-primary-light": theme.palette.primary.light,
"--color-secondary": theme.palette.secondary.main,
"--color-error": theme.palette.error.main,
"--color-warning": theme.palette.warning.main,
"--color-success": theme.palette.success.main,
// Spacing scale
"--spacing-xs": theme.spacing(0.5),
"--spacing-sm": theme.spacing(1),
"--spacing-md": theme.spacing(2),
"--spacing-lg": theme.spacing(3),
"--spacing-xl": theme.spacing(4),
// Typography
"--font-family": theme.typography.fontFamily,
"--font-size-sm": theme.typography.body2.fontSize,
"--font-size-md": theme.typography.body1.fontSize,
"--font-size-lg": theme.typography.h6.fontSize,
// Shadows and elevation
"--shadow-sm": theme.shadows[1],
"--shadow-md": theme.shadows[4],
"--shadow-lg": theme.shadows[8],
// Border radius
"--border-radius": theme.shape.borderRadius,
"--border-radius-lg": theme.shape.borderRadius * 2,
// Transitions
"--transition-fast": "0.15s ease",
"--transition-normal": "0.3s ease",
"--transition-slow": "0.5s ease"
},
// Dark mode variables
"[data-theme='dark']": {
"--color-background": "#1a1a1a",
"--color-surface": "#2d2d2d",
"--color-text": "#ffffff",
"--color-text-secondary": "#b3b3b3"
},
// Light mode variables
"[data-theme='light']": {
"--color-background": "#ffffff",
"--color-surface": "#f5f5f5",
"--color-text": "#333333",
"--color-text-secondary": "#666666"
}
}}
/>
);
}
// Using CSS variables in TSS styles
const useCSSVariableStyles = tss.create({
card: {
backgroundColor: "var(--color-surface)",
color: "var(--color-text)",
padding: "var(--spacing-lg)",
borderRadius: "var(--border-radius-lg)",
boxShadow: "var(--shadow-md)",
transition: "var(--transition-normal)",
"&:hover": {
boxShadow: "var(--shadow-lg)",
transform: "translateY(-2px)"
}
},
button: {
backgroundColor: "var(--color-primary)",
color: "white",
border: "none",
padding: "var(--spacing-sm) var(--spacing-md)",
borderRadius: "var(--border-radius)",
cursor: "pointer",
fontSize: "var(--font-size-md)",
transition: "var(--transition-fast)",
"&:hover": {
backgroundColor: "var(--color-primary-dark)"
}
}
});import { GlobalStyles } from "tss-react";
function ResponsiveGlobalStyles({ theme }: { theme: any }) {
return (
<GlobalStyles
styles={{
// Base typography scale
html: {
fontSize: 14,
[theme.breakpoints.up("sm")]: {
fontSize: 16
},
[theme.breakpoints.up("lg")]: {
fontSize: 18
}
},
// Container widths
".container": {
width: "100%",
maxWidth: "100%",
paddingLeft: theme.spacing(2),
paddingRight: theme.spacing(2),
marginLeft: "auto",
marginRight: "auto",
[theme.breakpoints.up("sm")]: {
maxWidth: 540,
paddingLeft: theme.spacing(3),
paddingRight: theme.spacing(3)
},
[theme.breakpoints.up("md")]: {
maxWidth: 720
},
[theme.breakpoints.up("lg")]: {
maxWidth: 960
},
[theme.breakpoints.up("xl")]: {
maxWidth: 1140
}
},
// Responsive grid system
".grid": {
display: "grid",
gap: theme.spacing(2),
gridTemplateColumns: "1fr",
[theme.breakpoints.up("sm")]: {
gridTemplateColumns: "repeat(2, 1fr)"
},
[theme.breakpoints.up("md")]: {
gridTemplateColumns: "repeat(3, 1fr)",
gap: theme.spacing(3)
},
[theme.breakpoints.up("lg")]: {
gridTemplateColumns: "repeat(4, 1fr)"
}
},
// Responsive utilities
".hide-mobile": {
display: "none",
[theme.breakpoints.up("md")]: {
display: "block"
}
},
".hide-desktop": {
display: "block",
[theme.breakpoints.up("md")]: {
display: "none"
}
}
}}
/>
);
}Install with Tessl CLI
npx tessl i tessl/npm-tss-react