Skip to content

Styling components

React has no styling system of its own. You write ordinary CSS, and JSX gives you two ways to attach it to an element: a class name, which covers almost everything, and a style prop for the handful of values a stylesheet has no way of knowing ahead of time. This chapter covers both, along with the pattern that sits between them: building a class name out of props or state, so an element restyles itself as your data changes.

Class names in JSX

class is a reserved word in JavaScript, so JSX names the attribute className. The value is a plain string, and React sets it as the element's class attribute in the DOM, which means the CSS on the other side is written exactly the way it always was.

jsx
function Avatar() {
  return <img className="avatar avatar--large" src={photo} alt="Ada Lovelace" />
}

Two class names in one string, separated by a space, the same as HTML. The camelCase rule from JSX applies to the attribute names in your markup; the class names themselves are yours, so kebab-case, BEM, or whatever convention your stylesheet already uses carries over untouched.

Importing a stylesheet

A component file pulls in the CSS it needs with a bare import at the top:

jsx
import './App.css'

export default function App() {
  return <h1 className="title">React Facts</h1>
}

There is no variable on the left of that import, and the component never reads the file. It is an instruction to the build tool: this module depends on that CSS, so include it. Vite injects the rules into the page during development and emits a real .css file in a production build. Setting up a React project describes the same mechanism handling images.

Where you put the import is a question of organization. Importing Button.css at the top of Button.jsx keeps the styles beside the component that uses them. The rules themselves stay global: a .title selector matches every element carrying that class anywhere on the page, whichever file happened to import it. Naming conventions like BEM exist to keep that shared namespace manageable, and CSS Modules and CSS-in-JS libraries scope the rules for you once an app outgrows naming discipline alone.

Version note

React 19 added support for rendering <link rel="stylesheet"> and <style> from inside a component, with a precedence prop that lets React hoist them into the document head and order them predictably. Earlier versions needed a library such as react-helmet. The build-tool import above behaves the same way in every version.

Class names that depend on data

className sits inside curly braces like any other JSX expression, so the string can be computed. Take a dice game where each die can be held between rolls. Whether a die is held lives in state higher up and arrives here as a prop:

jsx
function Die({ value, isHeld }) {
  return (
    <button className={`die ${isHeld ? 'die--held' : ''}`}>
      {value}
    </button>
  )
}

Every die gets die. A held one also gets die--held. The stylesheet holds the actual colors and borders, and the component's only job is deciding which class applies. When isHeld flips, React updates the class attribute and the browser repaints. Nothing inside the component knows what "held" looks like, so that appearance can change without this file changing at all.

One wrinkle: when isHeld is false, the template literal produces "die " with a trailing space. Browsers ignore it, though it looks untidy in devtools. An array handles it more cleanly and scales past one condition:

jsx
const classes = ['die', isHeld && 'die--held'].filter(Boolean).join(' ')

return <button className={classes}>{value}</button>

isHeld && 'die--held' evaluates to false when the condition fails, filter(Boolean) drops it, and join(' ') puts a single space between whatever survives.

The style prop

The style prop takes a JavaScript object. Property names are the camelCased versions of the CSS ones, and the double braces are the usual expression slot with an object literal inside it:

jsx
<div style={{ backgroundColor: 'darkslateblue', paddingTop: 12 }}>
  Styled inline
</div>

background-color becomes backgroundColor and padding-top becomes paddingTop. Values are usually strings. Pass a bare number and React appends px for properties that take a length, so paddingTop: 12 renders as padding-top: 12px. Properties that CSS treats as unitless keep the raw number: lineHeight, opacity, flexGrow, zIndex and fontWeight all pass straight through. Any other unit has to be written as a string, as in width: '60%' or margin: '2rem'.

Most styling belongs in a stylesheet, where one class can carry a dozen declarations and be shared by a dozen elements. The style prop earns its place when a value is only known at runtime and CSS has no way to reach it:

jsx
function ProgressBar({ percent }) {
  return (
    <div className="progress">
      <div className="progress__fill" style={{ width: `${percent}%` }} />
    </div>
  )
}

The fill's color, height, transition and corner radius all live in .progress__fill. The single value the stylesheet cannot know is the width, because it comes from data, so that one value goes inline. A background image whose URL arrives from an API has the same shape:

jsx
<div className="hero" style={{ backgroundImage: `url(${photo.url})` }} />

The sizing, positioning and overlay stay in .hero, and only the URL comes through the prop.

Inline styles also have a hard ceiling. A style object describes one element's declarations, so there is no way to express a hover state, a media query, or a keyframe animation in it. Those belong in CSS regardless of how dynamic the rest of the styling gets.

A single ternary inside a template literal reads fine. Two or three of them stacked in the same string turn into a wall of backticks and question marks that nobody wants to edit. Once a component has more than one condition, move the computation above the return and let the array form do the work:

jsx
function Button({ variant, size, isDisabled, isLoading, children }) {
  const classes = [
    'btn',
    `btn--${variant}`,
    `btn--${size}`,
    isDisabled && 'btn--disabled',
    isLoading && 'btn--loading',
  ]
    .filter(Boolean)
    .join(' ')

  return (
    <button className={classes} disabled={isDisabled}>
      {children}
    </button>
  )
}

Each line is one decision, so adding a fourth condition means adding a line rather than restructuring an expression, and the JSX stays readable because the attribute is a single identifier.

When several classes are mutually exclusive, a lookup object beats a chain of ternaries:

jsx
const statusClasses = {
  idle: 'card--idle',
  loading: 'card--loading',
  error: 'card--error',
}

const classes = ['card', statusClasses[status]].filter(Boolean).join(' ')

An unrecognized status yields undefined, which filter(Boolean) removes, so the element falls back to the base card class instead of rendering the literal text "undefined" into the attribute.

Once you are writing this in every component, the clsx and classnames packages do the same job in a few hundred bytes, with an object form for the conditional parts: clsx('btn', { 'btn--disabled': isDisabled }). They are a convenience over the array version above, which is worth understanding first.

The style prop takes an object because that maps directly onto how the DOM exposes styling. Every element has a style property, a CSSStyleDeclaration, whose members are the camelCased CSS property names: node.style.backgroundColor. React assigns those members individually. A string would mean writing cssText, which reparses and replaces every declaration on the element each time any one of them changes.

Assigning per property is what makes style updates diffable. React compares the previous style object with the next one key by key: keys whose values match are left alone, changed keys are written, and keys that disappeared are reset to an empty string. Only the properties that actually moved touch the DOM. A CSS string offers no way to express that partial update.

Vendor-prefixed properties follow the same camelCase rule with a capitalized first letter, as in WebkitLineClamp, with ms as the exception that stays lowercase (msOverflowStyle). React does not add prefixes on your behalf.

Custom properties are the one case where the key stays exactly as CSS spells it:

jsx
<div className="card" style={{ '--accent': team.color }}>

React sees the leading -- and routes it through setProperty, passing the value verbatim. No px is appended, so units are yours to supply. This is a useful bridge between the two approaches: set one custom property inline from data and let the stylesheet consume it across as many rules, pseudo-classes and media queries as it likes.

The identity cost is worth knowing about. An inline style object written straight into the JSX, as in the examples above, allocates a fresh object every render. On a host element that costs nothing measurable, since React compares the values rather than the reference and writes nothing when they match. It starts to matter when the object crosses a component boundary: pass a freshly built style object to a child wrapped in memo, and the shallow prop comparison sees a new reference every render and re-renders the child anyway, defeating the memoization. A className string has no such problem, because strings compare by value.

The fixes are ordinary: hoist static style objects to module scope so the reference is created once, and wrap dynamic ones in useMemo keyed on the values they derive from (see Hooks). Both are worth doing only where a profile shows the re-render costing something, and reaching for a class name removes the question altogether. Classes scale better in the browser anyway, since a matched rule set is shared across every element carrying the class, while inline declarations are repeated on each element and inflate server-rendered HTML accordingly.

JunoReach for a class first, inline style second Styling in React is the CSS you already write, attached through className because class is a reserved word in JavaScript. Import your stylesheet at the top of the file, put your rules in it, and let the component decide which class applies by building the string from props or state. Save the style prop for values your CSS file cannot know in advance, like a width that comes from data.
JunoReach for a class first, inline style secondclassName is an expression slot, so a template literal covers one condition and an array with filter(Boolean).join(' ') covers the rest. Compute it above the return so the JSX stays scannable, and use a lookup object when the classes are mutually exclusive. The style prop takes camelCased keys, appends px to bare numbers for length properties, and leaves unitless ones like opacity alone. It earns its place for runtime values, and it cannot express hover states or media queries at all.
JunoReach for a class first, inline style second The style object exists because it maps onto CSSStyleDeclaration, which lets React diff and write individual properties rather than reparsing a cssText string. Keys beginning with -- go through setProperty untouched, which makes a custom property the cleanest bridge from runtime data into a stylesheet. The trap is identity: a fresh object literal each render is invisible on a host element and fatal to a memo boundary, so hoist static objects to module scope, useMemo the dynamic ones, and prefer a class name where one will do.

Next up: Props, where components start taking data from their parent.