Semantic Markup in JSX

React 7,897 has no opinion about which element you render. <div> becomes a div, <button> becomes a button, and the tree is built from whatever comes out. Every role and keyboard behavior HTML gives free is a choice made in JSX, buried inside <PrimaryAction> where reviewers cannot see it.

Two spellings trip people up: attributes clashing with reserved words are renamed (className, htmlFor) and DOM properties are camelCased (tabIndex, readOnly), but aria-* and data-* keep their hyphens — aria-labelledby, never ariaLabelledBy.

Landmarks are the cheapest win: one <main>, a <nav> with an aria-label per block, a <header> and <footer>. Headings are harder, because a component rendering an <h2> cannot know how deeply its parent nested it. Pass the depth through context and let each heading pick its own tag.

Heading levels computed from contextJavaScript
const Level = createContext(1);
const Section = ({ children }) => <section>
  <Level.Provider value={useContext(Level) + 1}>{children}</Level.Provider></section>;
const Heading = ({ children }) => {
  const H = 'h' + Math.min(useContext(Level), 6);   // capitalized: React reads it as a tag
  return <H>{children}</H>;
};
const App = () => <>
  <a href="#main">Skip to content</a>
  <header><Heading>Web Coding Docs</Heading></header>
  <nav aria-label="Primary"><a href="/guide">Guide</a></nav>
  <main id="main"><Section><Heading>Routing</Heading>
    <Section><Heading>Nested routes</Heading></Section></Section></main></>;

The tree shows landmarks and an outline stepping 1, 2, 3 although no component wrote a number. The skip link comes first, or keyboard users retraverse the navigation on every route change.

Output of 114
RootWebArea "Docs"
  link "Skip to content"
  banner
    heading "Web Coding Docs" level=1
  navigation "Primary"
  main
    heading "Routing" level=2
    heading "Nested routes" level=3

Older advice says a wrapper <div> around <li> breaks semantics. Chromium 4,389 now ignores the generic wrapper and the list/listitem relationship survives — use Fragments anyway, for flex and grid parenting.