JSX Attributes
JSX attributes are how you configure the elements and components you render in React — they look almost exactly like HTML attributes, but under the hood they are just arguments passed to a JavaScript function call. Because JSX compiles down to React.createElement(type, props, children), every attribute you write becomes a key on a plain JavaScript props object. That single fact explains almost every rule in this lesson: why names are camelCase, why you can drop in any JavaScript expression with curly braces, and why some familiar HTML attribute names had to change.
Overview: How JSX Attributes Work
Overview: How JSX Attributes Work
When you write <img src="/cat.png" alt="A cat" />, the JSX compiler (Babel) transforms it into something like React.createElement("img", { src: "/cat.png", alt: "A cat" }). Every attribute in the tag becomes a property on that second argument, the props object. This is why JSX attributes behave differently from raw HTML attributes in a few important ways.
First, because the attribute name becomes a JavaScript object key, and because the DOM’s own property names (className, htmlFor, tabIndex, onClick) are camelCase in the DOM API that React talks to internally, JSX attribute names are camelCase too — not the all-lowercase or hyphenated names you may know from plain HTML. class becomes className, for becomes htmlFor, and event names like onclick become onClick.
Second, attribute values can be one of two things: a quoted string literal (name="value"), or a JavaScript expression wrapped in curly braces (name={expression}). Inside the curly braces you can put a variable, a function call, a ternary, an object literal — anything that is a valid JavaScript expression (not a statement; you cannot put an if block or a for loop directly inside {}).
Third, because React re-renders a component whenever its state or props change, attribute values are re-evaluated on every render. If an attribute is bound to a piece of state (for example disabled={isLoading}), React recalculates that expression each time the component function runs and updates only the DOM properties that actually changed — this is part of what the Virtual DOM diff does during reconciliation. Static string attributes never need to be recalculated, but expression-based attributes are exactly how you make an element interactive and reactive to data.
Attributes vs. props
There is no real distinction between a “JSX attribute” and a “prop” — they are the same thing from two different vantage points. When you write attributes on a lowercase tag like <input> or <div>, React maps known attributes onto real DOM properties (with translations like className → class). When you write attributes on a capitalized custom component like <UserCard name="Ana" />, React does not touch the DOM at all — it simply passes { name: "Ana" } as the props argument to your component function. Your component then decides what to do with that data.
Syntax
The general shape of a JSX opening tag with attributes looks like this:
<ElementName
staticAttribute="a string literal"
dynamicAttribute={someJsExpression}
booleanAttribute
{...spreadPropsObject}
/>
| Part | Meaning |
|---|---|
staticAttribute="..." |
A string literal value, written exactly like an HTML attribute. |
dynamicAttribute={...} |
Any JavaScript expression — a variable, function call, ternary, template string, object, or array — evaluated on every render. |
booleanAttribute |
Shorthand for booleanAttribute={true}. Common for props like disabled, required, checked, or your own custom boolean props. |
{...spreadPropsObject} |
Spreads every key of an object as individual attributes — useful for forwarding a bundle of props without listing each one. |
Examples
Example 1: Static and simple attributes
function ProfileCard() {
return (
<div className="profile-card">
<img src="/avatar.png" alt="User avatar" width="80" height="80" />
<h3>Jordan Lee</h3>
<a href="https://example.com/jordan" target="_blank" rel="noreferrer">
View profile
</a>
</div>
);
}
export default ProfileCard;
Renders a styled card containing an 80×80 avatar image with alt text, a heading reading “Jordan Lee”, and a link labeled “View profile” that opens in a new tab. Every attribute here is a plain string literal — no curly braces are needed because none of the values depend on JavaScript variables. Notice className instead of class, and that target="_blank" is paired with rel="noreferrer" for security (so the new tab cannot access window.opener).
Example 2: Dynamic attributes with expressions, booleans, and a style object
import { useState } from "react";
function SubscribeButton() {
const [isSubscribed, setIsSubscribed] = useState(false);
const buttonStyle = {
backgroundColor: isSubscribed ? "#4caf50" : "#2196f3",
color: "white",
padding: "8px 16px",
border: "none",
borderRadius: "4px",
};
return (
<button
style={buttonStyle}
disabled={isSubscribed}
onClick={() => setIsSubscribed(true)}
>
{isSubscribed ? "Subscribed" : "Subscribe"}
</button>
);
}
export default SubscribeButton;
Initially renders a blue button reading “Subscribe”. Clicking it calls setIsSubscribed(true), which triggers a re-render: the style object is recalculated so the background turns green, the disabled attribute becomes true so the button can no longer be clicked, and the label switches to “Subscribed”. This shows three attribute forms at once: an object expression (style), a boolean expression (disabled), and a function expression (onClick). Note that style in JSX always takes a JavaScript object with camelCase CSS properties (backgroundColor, not background-color) and unitless numbers are treated as pixels for properties like width — never a CSS string.
Example 3: Spreading a shared set of attributes
function IconButton({ label, ...rest }) {
return (
<button className="icon-button" aria-label={label} {...rest}>
{label}
</button>
);
}
function Toolbar() {
const commonProps = { type: "button", "data-testid": "toolbar-btn" };
return (
<div className="toolbar">
<IconButton
label="Save"
{...commonProps}
onClick={() => console.log("Saved")}
/>
<IconButton
label="Delete"
{...commonProps}
onClick={() => console.log("Deleted")}
/>
</div>
);
}
export default Toolbar;
Renders a toolbar with two buttons, “Save” and “Delete”. Each IconButton receives label, the spread commonProps (type and data-testid), and its own onClick. Inside IconButton, the {...rest} spread forwards every prop it didn’t explicitly destructure (here, type, data-testid, and onClick) onto the underlying <button>. Clicking “Save” logs Saved to the console; clicking “Delete” logs Deleted.
Output:
Saved
Deleted
How It Works Step by Step
On the first render (mount), React calls your component function, evaluates every JSX attribute expression once, and builds a Virtual DOM element tree with a props object per node. React then commits this tree to the real DOM, setting each corresponding DOM property or attribute (translating className to the DOM’s class, attaching event listeners for onClick-style props, and so on).
When state changes (for example, a useState setter is called), React re-renders the component: it re-runs the function body, which re-evaluates every attribute expression with the new values in scope. React then diffs the new Virtual DOM tree against the previous one during reconciliation. For attributes, this diff is shallow and per-property: if style‘s backgroundColor changed but padding did not, only the changed property is patched onto the real DOM node — React does not tear down and rebuild the element. This is why updating an attribute is cheap, and why you should let React own the DOM property rather than mutating it yourself with document.querySelector.
On unmount, if any attribute represented a subscription-like prop (for example an event handler tied to an external system via a ref and useEffect), React tears down the DOM node and its listeners; plain attributes require no special cleanup since they are just DOM properties.
Common Mistakes
Mistake 1: Using class instead of className.
function Badge() {
return <span class="badge">New</span>;
}
This is invalid because JSX attributes map to JavaScript object keys, and class is a reserved word in JavaScript — it cannot be used as a plain identifier the way HTML allows it as an attribute name. React will also warn that class is not a recognized DOM prop. The fix is to use className:
function Badge() {
return <span className="badge">New</span>;
}
Mistake 2: Passing style as a CSS string instead of an object.
function Alert() {
return <div style="color: red; font-weight: bold;">Error</div>;
}
In HTML, style is a semicolon-separated string. In JSX, style must be a JavaScript object with camelCase property names, because React sets each CSS property individually on element.style rather than assigning a raw string. Passing a string here throws a runtime error (or a `PropTypes`-style warning in dev). The corrected version:
function Alert() {
return (
<div style={{ color: "red", fontWeight: "bold" }}>Error</div>
);
}
The double curly braces are not special syntax — the outer braces start a JS expression, and the inner braces are an object literal being passed as that expression.
Mistake 3: Assuming a string value turns off a boolean attribute.
function SubmitButton() {
return <button disabled="false">Submit</button>;
}
Here "false" is a non-empty string, and any non-empty string is truthy — so the button stays disabled even though the intent was to enable it. Use a real boolean expression instead:
function SubmitButton({ isSaving }) {
return <button disabled={isSaving}>Submit</button>;
}
Best Practices
- Always use
classNameandhtmlFor, neverclassorfor, on JSX elements. - Pass
styleas an object with camelCase keys, and reach for a CSS class viaclassNamefor anything beyond a couple of dynamic properties. - Wrap any JavaScript expression — variables, ternaries, function calls, objects — in curly braces; only use quotes for genuine string literals.
- Never mutate a props or state object before passing it as an attribute; build a new object or array (
{ ...user, name },[...items, next]) so React can detect the change. - Use the boolean-shorthand (
disabledinstead ofdisabled={true}) for readability, but always pass a real boolean (not a string) when the value is dynamic. - Use
{...props}spreading sparingly and intentionally — it’s great for forwarding a known bag of props, but overusing it can hide which attributes a component actually accepts. - Give every attribute-driven list item a stable
keyprop drawn from real data (an id), never the array index, when the list can be reordered or filtered.
Practice Exercises
- Build a
StatusDotcomponent that accepts anisOnlineboolean prop and renders a<span>whosestyleobject setsbackgroundColorto green when online and gray when offline. - Fix this broken snippet so it compiles and behaves correctly:
<label class="field-label" for="email">Email</label>— rewrite it using the correct JSX attribute names. - Write a
LinkButtoncomponent that destructures alabelprop and spreads all remaining props onto an underlying<a>element, then render two instances with differenthrefandtargetvalues.
Summary
- JSX attributes compile into a JavaScript
propsobject, which is why their names are camelCase and their values can be any JS expression inside{}. - Use
classNameandhtmlForinstead of the HTML namesclassandfor, since those are reserved words in JavaScript. - The
styleattribute takes an object with camelCase CSS properties, not a CSS string. - Boolean attributes can use shorthand (
disabled) but dynamic booleans must be realtrue/falsevalues, not strings. - The spread operator (
{...obj}) forwards multiple attributes at once and is useful for wrapper components. - Attribute expressions are re-evaluated on every render, and React’s reconciliation only patches the DOM properties that actually changed.
