Inline Styles & Dynamic Styling
React's style prop accepts a plain JavaScript object. It is the lowest-overhead, most direct way to apply CSS to an element — no build step, no class name lookup, no external library. Inline styles are often dismissed as an anti-pattern, but they have a very specific sweet spot: truly dynamic, runtime-computed values that cannot be expressed as static class names.
Basic Syntax
The style prop takes a JavaScript object where CSS property names are written in camelCase and values are strings or numbers. Pixel values can be passed as plain numbers — React automatically appends px:
// Inline style object
function Badge({ color, children }: { color: string; children: React.ReactNode }) {
return (
<span
style={{
backgroundColor: color, // computed at runtime
color: 'white',
padding: '2px 10px', // string for non-pixel units
borderRadius: 999, // React appends 'px' → border-radius: 999px
fontSize: 12, // → font-size: 12px
fontWeight: 600,
display: 'inline-block',
}}
>
{children}
</span>
)
}
// Usage — any valid CSS color works
<Badge color="#0070f3">New</Badge>
<Badge color="#16a34a">Active</Badge>
<Badge color={user.brandColor}>Custom</Badge>Dynamic Styles Based on State
The most common use case for inline styles is tying a visual property directly to a piece of state — sizes, positions, percentages, or user- chosen values:
import { useState } from 'react'
function ProgressBar({ value, max = 100 }: { value: number; max?: number }) {
const pct = Math.min(100, Math.max(0, (value / max) * 100))
return (
<div
style={{
width: '100%',
height: 8,
background: '#e5e7eb',
borderRadius: 4,
overflow: 'hidden',
}}
role="progressbar"
aria-valuenow={value}
aria-valuemax={max}
>
<div
style={{
width: `${pct}%`, // computed — cannot be a static class
height: '100%',
background: pct > 80 ? '#16a34a' : pct > 40 ? '#f59e0b' : '#dc2626',
borderRadius: 4,
transition: 'width 0.4s ease, background 0.4s ease',
}}
/>
</div>
)
}
function Demo() {
const [progress, setProgress] = useState(0)
return (
<div>
<ProgressBar value={progress} />
<button onClick={() => setProgress((p) => Math.min(100, p + 10))}>
+10%
</button>
</div>
)
}CSS Custom Properties (Variables) with Inline Styles
CSS custom properties are the most powerful pattern for combining inline styles with class-based CSS. The inline style prop sets the variable value; the CSS class consumes it. This gives you dynamic values without sacrificing pseudo-selectors, media queries, or animations:
// The CSS class handles structure and animations
// The inline style passes the dynamic values as custom properties
function Avatar({
src,
size = 40,
borderColor = '#0070f3',
}: {
src: string
size?: number
borderColor?: string
}) {
return (
<img
src={src}
className="avatar" // defined in a CSS Module or global stylesheet
style={{
'--avatar-size': `${size}px`, // CSS custom property
'--avatar-border': borderColor,
} as React.CSSProperties}
/>
)
}/* avatar.css (or Avatar.module.css) */
.avatar {
width: var(--avatar-size, 40px);
height: var(--avatar-size, 40px);
border-radius: 50%;
border: 2px solid var(--avatar-border, #d1d5db);
object-fit: cover;
transition: border-color 0.2s ease;
}
.avatar:hover {
/* pseudo-selectors still work — impossible with pure inline styles */
filter: brightness(1.05);
box-shadow: 0 0 0 3px color-mix(in srgb, var(--avatar-border) 30%, transparent);
}Building an Animated Color Picker
Here is a more complete example: a color swatch picker where the preview is driven entirely by inline styles because the color value is user input:
import { useState } from 'react'
const PRESET_COLORS = ['#ef4444', '#f59e0b', '#10b981', '#3b82f6', '#8b5cf6', '#ec4899']
function ColorPicker({ onChange }: { onChange?: (color: string) => void }) {
const [selected, setSelected] = useState(PRESET_COLORS[3])
function pick(color: string) {
setSelected(color)
onChange?.(color)
}
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
{/* Preview */}
<div
style={{
width: 64,
height: 64,
borderRadius: 12,
background: selected,
boxShadow: `0 4px 12px ${selected}66`,
transition: 'background 0.2s ease, box-shadow 0.2s ease',
}}
/>
{/* Swatches */}
<div style={{ display: 'flex', gap: 8 }}>
{PRESET_COLORS.map((color) => (
<button
key={color}
onClick={() => pick(color)}
style={{
width: 28,
height: 28,
borderRadius: '50%',
background: color,
border: selected === color ? '3px solid #111' : '2px solid transparent',
outline: selected === color ? `2px solid ${color}` : 'none',
outlineOffset: 2,
cursor: 'pointer',
transition: 'transform 0.1s ease',
transform: selected === color ? 'scale(1.15)' : 'scale(1)',
}}
aria-label={`Pick color ${color}`}
/>
))}
</div>
{/* Freeform input */}
<input
type="color"
value={selected}
onChange={(e) => pick(e.target.value)}
style={{ width: 40, height: 32, cursor: 'pointer', borderRadius: 4 }}
/>
</div>
)
}Limitations of Inline Styles
Feature | Inline styles | Workaround |
|---|---|---|
Pseudo-selectors (:hover, :focus) | Not supported | Use CSS class with state toggle or CSS custom props |
Media queries | Not supported | Use CSS class or JS window resize listener |
CSS animations / @keyframes | Not supported | Use CSS class + CSS custom prop for values |
Pseudo-elements (::before) | Not supported | Must use CSS class |
CSS variables consumption | Must use var() | Works fine as a pattern |
Performance (many updates) | Each update diffs the object | Memoize with useMemo for stable objects |
Combining Inline Styles with CSS Modules
The gold standard is: use a CSS Module class for everything that is static (layout, typography, pseudo-selectors, animations), and use inline styles only for the values that change at runtime:
import styles from './Tooltip.module.css'
interface TooltipProps {
text: string
x: number // cursor position — runtime computed
y: number
visible: boolean
}
function Tooltip({ text, x, y, visible }: TooltipProps) {
return (
<div
className={styles.tooltip} // handles border-radius, shadow, typography
style={{
left: x + 12, // dynamic — cursor position
top: y - 8,
opacity: visible ? 1 : 0, // could also be a CSS class toggle
pointerEvents: visible ? 'auto' : 'none',
}}
>
{text}
</div>
)
}Performance Considerations
React compares the previous and next style objects on every render. For components that re-render frequently (e.g., inside animations), create the style object outside the component or memoize it so React gets a stable reference:
import { useMemo } from 'react'
function AnimatedNode({ x, y, color }: { x: number; y: number; color: string }) {
// Memoize — only recomputes when x, y, or color changes
const style = useMemo(
() => ({
position: 'absolute' as const,
left: x,
top: y,
background: color,
width: 12,
height: 12,
borderRadius: '50%',
}),
[x, y, color],
)
return <div style={style} />
}Use inline styles for truly dynamic runtime values: positions, percentages, user-chosen colors
Use CSS custom properties when you also need pseudo-selectors or animations on the same element
Combine with CSS Modules — classes for structure, inline for dynamics
Memoize style objects in hot render paths to avoid unnecessary React object diffs
Avoid inline styles for static values — every static inline style is a missed caching opportunity for the browser