Get eBook BundleVolume I index
JSX Is Real React Code

Attributes, Class Names, and Styles

Ishtmeet Singh @ishtms/July 20, 2026/12 min read
#react#jsx#attributes#css#accessibility

You've already been writing stuff like this in JSX:

jsx
<a className="lesson-link" href="/learn/jsx">
  Read the JSX chapter
</a>

And maybe you didn't think much about what className and href actually are here. They look pretty much like HTML attributes, right?

Well, inside JSX they're props on the React element.

Since <a> is a lowercase element, React DOM knows this is supposed to become a real browser anchor. So when React DOM handles this element, it takes those props and applies whatever needs applying to the actual DOM node. className ends up as the browser's class, href becomes the link destination, and so on.

This gets slightly more interesting once custom components come in, because props on your own component don't automatically mean anything to the browser.

Let's see that properly.

Host props and component props mean different things

This is a normal browser button written in JSX:

jsx
<button type="button" disabled={false}>
  Continue
</button>

type and disabled are props React DOM knows how to handle for a button.

Now look at this:

jsx
<ContinueButton available lessonId="jsx" />

available and lessonId are completely up to us.

React DOM doesn't see <ContinueButton /> and decide that lessonId must become some browser attribute. ContinueButton is our function, so our function decides what those values actually do.

jsx
function ContinueButton({ available, lessonId }) {
  return (
    <button type="button" disabled={!available}>
      Continue {lessonId}
    </button>
  );
}

So available gets used to calculate disabled, and lessonId ends up inside the button text.

If you inspect the actual browser DOM, you're not going to find an available prop sitting there. The component consumed it before React DOM ever dealt with the real <button>.

This is a useful separation to remember. Props on your own components are your API. Props on lowercase elements are interpreted by React DOM according to what that browser element supports.

Why JSX uses className

This one everybody runs into almost immediately.

Normal HTML uses:

html
<article class="lesson-card">Props as Inputs</article>

JSX normally uses:

jsx
<article className="lesson-card">Props as Inputs</article>

The browser still gets a normal class attribute in the end. className is the prop name you use from JSX.

And yep, multiple classes are still just one string:

jsx
<article className="lesson-card lesson-card--featured">
  Props as Inputs
</article>

Nothing special happening there.

If the classes depend on some data, you can calculate the string in JavaScript before returning the JSX.

jsx
const className = featured
  ? "lesson-card lesson-card--featured"
  : "lesson-card";

return <article className={className}>{title}</article>;

Every time this component renders, className gets calculated from the current featured value. If the result changed, React DOM updates the class on the browser element.

For one or two conditions, this is usually all you need.

Conditional classes can get annoying pretty fast

Suppose a lesson card can be featured and complete independently.

You could do this:

jsx
const classes = ["lesson-card"];

if (featured) classes.push("lesson-card--featured");
if (complete) classes.push("lesson-card--complete");

const className = classes.join(" ");

Then:

jsx
return <article className={className}>{title}</article>;

That's perfectly fine. The array only exists during this render, you build the class string, and then you're done with it.

You can also generate a class from a known value:

jsx
const className = `status status--${status}`;

If status can only be something you expect, say "reading" or "complete", this works nicely.

Just don't blindly dump random values into class names and assume CSS exists for them. If status somehow becomes "potato", React will happily produce:

html
class="status status--potato"

React doesn't know whether your stylesheet has a rule for that. That's your code's job.

Once a project starts repeating larger combinations of classes everywhere, a class helper library can save some typing. But for two boolean conditions? I'd probably just write the JavaScript and move on.

Some HTML names change in JSX

className isn't the only one.

You'll also see stuff like:

jsx
<label htmlFor="reader-email">Email</label>
<input id="reader-email" readOnly tabIndex={0} />

In HTML you'd normally write for, readonly, and tabindex.

In JSX, these become htmlFor, readOnly, and tabIndex.

A lot of React DOM prop names follow the DOM property names, which is why camel casing shows up everywhere.

But don't go camel-casing every attribute you see, because some of them stay exactly as they're normally written.

ARIA attributes stay hyphenated:

jsx
<button
  aria-expanded={false}
  aria-controls="chapter-details"
>
  Show details
</button>

And data-* attributes do too:

jsx
<div data-chapter-id="jsx-real-react-code" />

So this:

jsx
aria-expanded
data-chapter-id

stays hyphenated.

While this:

jsx
readOnly
tabIndex
htmlFor

uses the React DOM prop name.

If you get one wrong in development, React will often warn you and tell you what it expected. Which is nice, because remembering every DOM prop spelling from memory would be a pretty pointless use of brain space.

Boolean props should actually be booleans

Take disabled.

This is normal:

jsx
<button disabled={!available}>Continue</button>

!available evaluates to either true or false, and that's what React receives.

Now this:

jsx
<button disabled="false">Continue</button>

looks reasonable for about three seconds.

But "false" is a string. It isn't JavaScript's boolean false.

For HTML boolean attributes such as disabled, the presence of the attribute is what counts. So giving it a string called "false" doesn't mean "please enable this button". You've still supplied the attribute.

If you mean false, write false:

jsx
<button disabled={false}>Continue</button>

And if you mean true, JSX has the short form:

jsx
<button disabled>Continue</button>

That's the same as:

jsx
<button disabled={true}>Continue</button>

You'll use this with props such as required, multiple, hidden, readOnly, and so on.

ARIA needs a little more care here. Something such as:

jsx
aria-expanded={false}

is intentionally communicating a false state. You don't remove it just because the JavaScript value happens to be false. ARIA attributes follow their own allowed values and meanings.

React doesn't fix bad HTML for you

This is one area where people sometimes expect React to help more than it does.

Take a normal text button:

jsx
<button type="button">Save reading goal</button>

The visible text gives the button its accessible name.

Now say the button only contains an icon:

jsx
<button type="button" aria-label="Close chapter menu">
  <CloseIcon aria-hidden="true" />
</button>

Now we provide a name explicitly because there isn't useful visible text saying what the button does. The icon itself can be hidden from assistive technology because the button already has the name we want announced.

Forms are similar.

jsx
<label htmlFor="reader-name">Reader name</label>
<input id="reader-name" name="name" />

The label's htmlFor points at the input's id, so the browser can associate those two elements.

A placeholder doesn't replace this:

jsx
<input placeholder="Reader name" />

Placeholder text disappears once the user starts typing, and it doesn't provide the same labeling behavior.

React doesn't inspect your <div> and go, "hmm, Ish probably meant a button here."

If you use the wrong HTML element, it's still the wrong HTML element. JSX changes how you write the markup in JavaScript, but normal HTML semantics still come from the browser.

Images still need proper alt text

Same story with images.

If the image communicates something useful, give it alternative text describing the information or purpose you need conveyed.

jsx
<img
  src="/covers/volume-1.png"
  alt="ReactBook Volume I cover"
/>

If it's only decorative:

jsx
<img src="/stars.svg" alt="" />

That empty string is intentional.

You're telling assistive technology that the image doesn't need to be announced.

Leaving alt out completely isn't the same instruction. Depending on the browser and assistive technology, you can end up with stuff such as the filename being announced, which is obviously not what you wanted.

Also, if nearby visible text already communicates everything the image does, an empty alt can stop the same information getting announced twice.

Again, none of this changes just because we're inside React.

The style prop is a JavaScript object

This syntax looks weird the first time you see it:

jsx
<div style={{ width: "50%", backgroundColor: "#61dafb" }} />

Why two pairs of braces?

The outer {} means "we're putting a JavaScript expression inside JSX".

The inner {} is the actual JavaScript object.

So this:

jsx
style={{ width: "50%" }}

is really just passing this object:

js
{ width: "50%" }

to the style prop.

You can make that more obvious by storing it first:

jsx
const progressStyle = {
  width: "50%",
  backgroundColor: "#61dafb",
};

return <div style={progressStyle} />;

Same result.

CSS property names in the object normally use camel case:

text
background-color -> backgroundColor
margin-top       -> marginTop
z-index          -> zIndex

And this HTML-style string isn't how React DOM's normal style prop works:

jsx
<div style="width: 50%" />

React expects the object.

Numbers inside style objects can get units automatically

You can write:

jsx
<div style={{ width: 320, marginTop: 16 }} />

For properties where pixel units make sense, React DOM will use pixel values, so those become 320px and 16px.

But some CSS properties use plain numbers already.

jsx
<p style={{ opacity: 0.8, lineHeight: 1.6, zIndex: 2 }}>
  Current lesson
</p>

You wouldn't want opacity: 0.8px, obviously.

And if you want some other CSS unit, write it yourself:

jsx
<main style={{ minHeight: "100vh", maxWidth: "70rem" }} />

So don't build some helper that blindly sticks "px" after every number. CSS properties don't all use values the same way.

When should you use classes and when should you use style?

Most normal visual rules can stay in CSS.

css
.lesson-link {
  background: #61dafb;
  color: #06141b;
}

.lesson-link:focus-visible {
  outline: 3px solid white;
}

Then the component just supplies the class:

jsx
<a className="lesson-link" href="/learn">
  Start reading
</a>

This works well for normal styles, hover states, focus states, media queries, animations, and all the other stuff CSS already knows how to do.

Inline style starts making more sense when a value is coming directly from runtime data.

Say we have a percentage:

jsx
const width = `${percentage}%`;

return (
  <div
    className="progress-fill"
    style={{ width }}
  />
);

The class can hold all the normal progress-bar styling, while the width comes from the current percentage.

That's a pretty common split.

You don't need to turn every style into JavaScript just because you're using React.

Creating a style object during render is fine

You might see code like this:

jsx
function Progress({ percentage }) {
  return <div style={{ width: `${percentage}%` }} />;
}

That's okay.

A new object gets created during the render. React supports that just fine.

If some style never changes, you can put the object outside the component:

jsx
const decorativeStyle = {
  pointerEvents: "none",
  userSelect: "none",
};

Then reuse it:

jsx
function Decoration() {
  return <div style={decorativeStyle} />;
}

Just don't start mutating that shared object later.

And don't immediately start adding useMemo() around every style object because somebody told you object creation is expensive. If you eventually have a real reason for stable object identity, deal with it then.

For ordinary JSX, creating a small style object during render is normal.

Be careful when spreading props onto DOM elements

You can build wrapper components that forward DOM props.

For example:

jsx
function TextField({ className = "", ...inputProps }) {
  return (
    <input
      {...inputProps}
      className={`text-field ${className}`.trim()}
    />
  );
}

Here we're taking className out first, collecting everything else into inputProps, then forwarding those props onto the real <input>.

After that, we create the final class name ourselves.

The order matters when the same prop appears twice.

Look at this:

jsx
<input className="fixed" {...inputProps} />

If inputProps contains its own className, the later spread can replace "fixed".

Swap them:

jsx
<input {...inputProps} className="fixed" />

Now our "fixed" value comes later, so it wins.

Usually, if both values are supposed to be supported, it's better to combine them deliberately instead of relying on which one happened to appear last.

Also, don't do stuff like this with random application data:

jsx
<article {...lesson} />

Maybe lesson contains title, database IDs, timestamps, internal status fields, author records, API metadata, or other stuff that has no business being passed to an <article>.

Pick the DOM props your component actually supports and pass those.

Check what actually reached the browser

When you're confused about one of these props, browser DevTools helps a lot.

Suppose you wrote:

jsx
<article
  className="lesson-card"
  data-status="complete"
  style={{ width: "50%" }}
>
  JSX
</article>

Open the Elements panel.

You can check what class actually ended up on the DOM node, whether data-status exists, which inline styles arrived, and what the browser's computed CSS finally became.

This is useful because your JSX can be completely correct while the page still looks wrong. Maybe another CSS selector overrides the value. Maybe the element inherited something. Maybe your wrapper component never forwarded the prop you thought it did.

React DevTools and the normal Elements panel answer slightly different questions here.

React DevTools lets you inspect your React components and the props they received.

The Elements panel shows the actual DOM React DOM produced.

If you've got a wrapper component between those two, checking both usually tells you where the value changed.

Follow one prop all the way through

Let's finish with one simple example:

jsx
function LessonLink({ featured }) {
  const className = featured
    ? "lesson-link lesson-link--featured"
    : "lesson-link";

  return (
    <a
      className={className}
      href="/learn/jsx"
      aria-current={featured ? "page" : undefined}
    >
      Read JSX
    </a>
  );
}

featured is our component prop. It only means whatever LessonLink decides it means.

The component uses it to calculate className and aria-current.

Then it returns a lowercase <a>, so now React DOM gets involved with those host props.

React DOM applies the class, link destination, and ARIA attribute to the real browser anchor.

After that, the browser handles the normal anchor behavior, CSS matching, focus behavior, accessibility information, and rendering.

So when you're looking at some JSX prop and wondering what actually happens to it, follow it through those steps.

First ask whether the prop belongs to your own component or to a lowercase browser element. Then see what the component returns. After that, inspect the actual DOM and see what React DOM applied.

Once that becomes normal, stuff like className, htmlFor, aria-*, data-*, style objects, and prop spreading stops feeling like random JSX syntax.

It's just JavaScript values going through your component code, then React DOM applying the host props that eventually reach the browser.