Learn how to build custom components that integrate seamlessly with Theme UI’s theming system.
Using the sx Prop
The simplest way to create themed components is using the sx prop on standard HTML elements.
Basic Example
/** @jsxImportSource theme-ui */
function Alert({ children }) {
return (
<div
sx={{
padding: 3,
borderRadius: 2,
bg: 'muted',
borderLeft: '4px solid',
borderColor: 'primary',
}}
>
{children}
</div>
)
}
Accepting sx as a Prop
Allow consumers to customize your component:
import type { SxProp } from 'theme-ui'
interface AlertProps extends SxProp {
children: React.ReactNode
}
function Alert({ children, sx }: AlertProps) {
return (
<div
sx={{
padding: 3,
borderRadius: 2,
bg: 'muted',
borderLeft: '4px solid',
borderColor: 'primary',
// Merge with custom styles
...sx,
}}
>
{children}
</div>
)
}
// Usage - override background
<Alert sx={{ bg: 'highlight' }}>Custom alert</Alert>
The sx prop merges styles, with later styles overriding earlier ones.
Using the Box Component
The Box component is a versatile primitive for building themed components.
Box as a Building Block
import { Box } from 'theme-ui'
function Card({ title, children, ...props }) {
return (
<Box
{...props}
sx={{
padding: 4,
borderRadius: 2,
bg: 'background',
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)',
}}
>
<Box as="h3" sx={{ fontSize: 3, mb: 2 }}>
{title}
</Box>
{children}
</Box>
)
}
Box Props
The Box component accepts several useful props:
import { Box } from 'theme-ui'
// 'as' prop - render as different element
<Box as="section">Section element</Box>
<Box as="article">Article element</Box>
// System props - shorthand for common styles
<Box
m={3} // margin: theme.space[3]
p={2} // padding: theme.space[2]
bg="primary" // background: theme.colors.primary
color="white"
>
Content
</Box>
// Variant prop - apply theme variants
<Box variant="cards.primary">
Styled like a card
</Box>
Available System Props
Box supports these shorthand props:
// Margin
m, mt, mr, mb, ml, mx, my
margin, marginTop, marginRight, marginBottom, marginLeft, marginX, marginY
// Padding
p, pt, pr, pb, pl, px, py
padding, paddingTop, paddingRight, paddingBottom, paddingLeft, paddingX, paddingY
// Color
color, backgroundColor, bg, opacity
Component Composition
Build complex components by composing simpler ones.
Example: Feature Card
import { Box, Heading, Text, Button } from 'theme-ui'
interface FeatureCardProps {
icon: React.ReactNode
title: string
description: string
action: () => void
}
function FeatureCard({ icon, title, description, action }: FeatureCardProps) {
return (
<Box
sx={{
padding: 4,
borderRadius: 2,
bg: 'background',
boxShadow: 'medium',
transition: 'transform 0.2s',
'&:hover': {
transform: 'translateY(-4px)',
boxShadow: 'large',
},
}}
>
<Box sx={{ fontSize: 5, mb: 3 }}>
{icon}
</Box>
<Heading as="h3" sx={{ mb: 2 }}>
{title}
</Heading>
<Text sx={{ mb: 3, color: 'text', lineHeight: 'body' }}>
{description}
</Text>
<Button onClick={action} variant="primary">
Learn More
</Button>
</Box>
)
}
Polymorphic Components
Create components that can render as different elements:
import { Box, BoxProps } from 'theme-ui'
import { ComponentProps, ElementType } from 'react'
interface PolymorphicBoxProps<T extends ElementType> extends BoxProps {
as?: T
}
type Props<T extends ElementType> = PolymorphicBoxProps<T> &
Omit<ComponentProps<T>, keyof PolymorphicBoxProps<T>>
function Container<T extends ElementType = 'div'>({
as,
...props
}: Props<T>) {
return (
<Box
as={as || 'div'}
sx={{
maxWidth: 1024,
mx: 'auto',
px: 3,
}}
{...props}
/>
)
}
// Usage
<Container>Div container</Container>
<Container as="section">Section container</Container>
<Container as="main">Main container</Container>
Using Variants
Create components that support theme variants.
Component with Variants
import { Box } from 'theme-ui'
interface BadgeProps {
variant?: 'success' | 'warning' | 'error' | 'info'
children: React.ReactNode
}
function Badge({ variant = 'info', children }: BadgeProps) {
return (
<Box
__themeKey="badges"
variant={variant}
sx={{
display: 'inline-block',
px: 2,
py: 1,
fontSize: 0,
borderRadius: 1,
fontWeight: 'bold',
}}
>
{children}
</Box>
)
}
// Theme configuration
const theme = {
badges: {
success: { bg: 'green', color: 'white' },
warning: { bg: 'yellow', color: 'black' },
error: { bg: 'red', color: 'white' },
info: { bg: 'blue', color: 'white' },
},
}
// Usage
<Badge variant="success">Success</Badge>
<Badge variant="error">Error</Badge>
Custom Variant Keys
Define your own variant location in the theme:
import { Box } from 'theme-ui'
interface PanelProps {
variant?: string
children: React.ReactNode
}
function Panel({ variant, children }: PanelProps) {
return (
<Box
__themeKey="panels" // Custom theme key
variant={variant}
>
{children}
</Box>
)
}
// Theme
const theme = {
panels: {
default: {
padding: 3,
bg: 'background',
},
highlighted: {
padding: 3,
bg: 'primary',
color: 'white',
},
},
}
Accessing the Theme
Use the useThemeUI hook to access theme values in your components.
Basic Usage
import { useThemeUI } from 'theme-ui'
function ThemedComponent() {
const { theme } = useThemeUI()
return (
<div
style={{
// Access raw theme values
color: theme.colors?.primary as string,
padding: theme.space?.[3] as number,
}}
>
Themed content
</div>
)
}
Theme color values are CSS custom properties. Use theme.rawColors if you need raw color values (e.g., for Canvas or WebGL).
Using Raw Colors
import { useThemeUI } from 'theme-ui'
function CanvasComponent() {
const { theme } = useThemeUI()
const { rawColors } = theme
// Use rawColors for contexts that don't support CSS variables
const primaryColor = rawColors?.primary
// Use in canvas, chart libraries, etc.
return <canvas ref={ref => {
const ctx = ref?.getContext('2d')
if (ctx) {
ctx.fillStyle = primaryColor as string
}
}} />
}
Color Mode
Access and control color mode:
import { useColorMode } from 'theme-ui'
function ColorModeToggle() {
const [colorMode, setColorMode] = useColorMode()
return (
<button
onClick={() => {
setColorMode(colorMode === 'light' ? 'dark' : 'light')
}}
sx={{
padding: 2,
bg: 'primary',
color: 'white',
border: 'none',
borderRadius: 2,
cursor: 'pointer',
}}
>
Toggle {colorMode === 'light' ? 'Dark' : 'Light'} Mode
</button>
)
}
Forwarding Refs
Create components that support React refs:
import { forwardRef } from 'react'
import type { BoxProps } from 'theme-ui'
interface InputProps extends Omit<BoxProps, 'as'> {
label: string
error?: string
}
const Input = forwardRef<HTMLInputElement, InputProps>(
({ label, error, ...props }, ref) => {
return (
<div>
<label
sx={{
display: 'block',
mb: 1,
fontSize: 1,
fontWeight: 'bold',
}}
>
{label}
</label>
<input
ref={ref}
sx={{
width: '100%',
padding: 2,
fontSize: 2,
borderRadius: 1,
border: '1px solid',
borderColor: error ? 'error' : 'muted',
'&:focus': {
outline: 'none',
borderColor: 'primary',
},
...props.sx,
}}
{...props}
/>
{error && (
<div sx={{ mt: 1, fontSize: 0, color: 'error' }}>
{error}
</div>
)}
</div>
)
}
)
Input.displayName = 'Input'
Using the css Helper
For more complex styling needs, use the css helper directly:
import { css, useThemeUI } from 'theme-ui'
function ComplexComponent() {
const { theme } = useThemeUI()
const styles = css({
position: 'relative',
'&::before': {
content: '""',
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
bg: 'primary',
opacity: 0.1,
},
})(theme)
return <div css={styles}>Content</div>
}
Best Practices
Accept sx Prop
Always accept an sx prop to allow consumers to customize your components.
Use Box Component
Build on top of the Box component for consistent theming and system props.
Leverage Variants
Use theme variants for common style variations instead of multiple props.
TypeScript Types
Export proper TypeScript types for your components’ props.
Complete Example
Here’s a complete example combining all concepts:
import { forwardRef } from 'react'
import { Box, BoxProps, useThemeUI } from 'theme-ui'
import type { SxProp } from 'theme-ui'
interface CardProps extends SxProp {
variant?: 'default' | 'elevated' | 'outlined'
title?: string
footer?: React.ReactNode
children: React.ReactNode
}
export const Card = forwardRef<HTMLDivElement, CardProps>(
({ variant = 'default', title, footer, children, sx }, ref) => {
const { theme } = useThemeUI()
return (
<Box
ref={ref}
__themeKey="cards"
variant={variant}
sx={{
padding: 4,
borderRadius: 2,
bg: 'background',
...sx,
}}
>
{title && (
<Box
as="h3"
sx={{
fontSize: 3,
fontWeight: 'bold',
mb: 3,
pb: 2,
borderBottom: '1px solid',
borderColor: 'muted',
}}
>
{title}
</Box>
)}
<Box sx={{ mb: footer ? 3 : 0 }}>
{children}
</Box>
{footer && (
<Box
sx={{
pt: 3,
borderTop: '1px solid',
borderColor: 'muted',
}}
>
{footer}
</Box>
)}
</Box>
)
}
)
Card.displayName = 'Card'
// Theme configuration
export const theme = {
cards: {
default: {
boxShadow: 'none',
},
elevated: {
boxShadow: '0 4px 16px rgba(0, 0, 0, 0.12)',
transition: 'transform 0.2s',
'&:hover': {
transform: 'translateY(-2px)',
boxShadow: '0 8px 24px rgba(0, 0, 0, 0.16)',
},
},
outlined: {
border: '1px solid',
borderColor: 'muted',
boxShadow: 'none',
},
},
}
// Usage
<Card
variant="elevated"
title="Card Title"
footer={<button>Action</button>}
sx={{ maxWidth: 400 }}
>
Card content goes here
</Card>