A closed vocabulary
You never write a raw HTML tag. Semantic elements come from a typed menu on the primitives — and the menu is short on purpose.
When a layout primitive needs to be a landmark or a list item, it does not become a raw <section> or <li>. It takes an as prop whose type is a closed union of allowed elements. Ask for one that is not on the menu — asa button, say — and it does not compile, because the system's answer to a button is the Button component.
Raw tags do not merely go unrecommended; they fail the lint. The no-naked-element gate runs at error on every build, so an ordinary element in application code is a broken build, not a style nit. Between the typed union and the gate, the set of elements you can reach for is finite and known.
That is the whole payoff: a bounded menu of decisions in place of the unbounded surface of HTML. There are fewer ways to be wrong, and every right answer has exactly one spelling.
The whole menu
There is no unbounded HTML to reach for — only this finite set of decisions. Each raw tag maps to exactly one thing you write instead, and reaching for the tag itself fails the lint.
| Raw element | What you write instead |
|---|---|
<a> | Anchor |
<article> | Box as="article" |
<aside> | Box as="aside" |
<button> | Button |
<code> | Code |
<dd> | Text as="dd" |
<div> | Box (decorative) / Stack | Flex | Grid (layout) |
<dt> | Text as="dt" |
<figcaption> | Text as="figcaption" |
<figure> | Box as="figure" |
<footer> | Box as="footer" |
<form> | Form |
<h1> | Heading level={1} |
<h2> | Heading level={2} |
<h3> | Heading level={3} |
<h4> | Heading level={4} |
<h5> | Heading level={5} |
<h6> | Heading level={6} |
<header> | Box as="header" |
<img> | GalleryImage |
<input> | Input |
<label> | Label |
<legend> | Text as="legend" |
<li> | Box as="li" / Text as="li" |
<main> | Container as="main" / Box as="main" |
<nav> | Box as="nav" |
<ol> | Stack as="ol" |
<p> | Text |
<pre> | Code block |
<section> | Box as="section" / Section |
<select> | Select |
<span> | Box as="span" (decorative) / Text as="span" (textual) |
<svg> | Icon |
<textarea> | Textarea |
<ul> | Stack as="ul" |
One way to draw an icon
An icon is not a free SVG either. Raw glyphs arrive at whatever size and stroke the author happens to type; the Icon component collapses that to a single decision — a size token — and renders everything at a consistent 18px, stroke 1.5.
Raw Lucide
Whatever size and stroke each call site happens to set.
<ArrowRight className="size-6 stroke-2" />Icon component
One size token in, a fixed 18px / stroke 1.5 out — every time.
<Icon icon={ArrowRight} size="sm" />The one escape hatch
Primitives still take className, so genuine one-off layout is never blocked. But the hatch is policed: the tokens-only gate rejects raw hex, colour functions, and arbitrary values in class strings, so even a bespoke tweak has to be spelled in the design language. The levers are open; the vocabulary stays closed. The rules behind both live on the doctrine page.