Elements & attributes
How JSX elements and attributes map to HTML.
Lowercase elements are HTML#
A lowercase tag is an HTML element; an uppercase one is a component.
Void elements
(img, br, input, …) render without a closing tag.
import { renderToString } from "@vincle/core";
const html = await renderToString( <> <input type="text" /> <br /> </>,);<input type="text" /><br />A void element takes no children#
HTML has no form for content inside a void element: a parser would move it.
So any children other than undefined throws, even a value that renders
nothing ({list.length > 0 && <b>x</b>} throws on an empty list too), and the
mistake shows in development, not only when real data arrives.
[vincle/core] <img> is a void element and cannot have children: move the content next to it, not inside.<img src={src}>Caption</img> // ❌ throws<img src={src} alt="Caption" /> // ✅<figure><img src={src} /><figcaption>Caption</figcaption></figure> // ✅The error is thrown when the element is created, so JSX built outside a
component throws before renderToString is called. To catch it while writing,
enable
void-dom-elements-no-children
(eslint-plugin-react, ported by oxlint). It flags explicit children only:
children passed through a spread still reach the runtime check.
Attributes#
Most names pass through unchanged. The React spellings map to their HTML names:
| JSX name | HTML attribute |
|---|---|
className |
class |
htmlFor |
for |
tabIndex |
tabindex |
readOnly |
readonly |
autoComplete |
autocomplete |
colSpan |
colspan |
httpEquiv |
http-equiv |
acceptCharset |
accept-charset |
When both spellings reach one element, the HTML name wins and the React one is dropped: no merge, no warning. Prefer the HTML names: they match the output.
import { renderToString } from "@vincle/core";
const html = await renderToString(<div className="foo" class="bar" />);<div class="bar"></div>Boolean and falsy values#
null and undefined omit the attribute. On an HTML boolean attribute
(disabled, hidden, checked…), false omits it and true renders the bare
name; on any other attribute, true and false render as text.
<button disabled={isBusy}>Save</button>// isBusy true → <button disabled>Save</button>// isBusy false → <button>Save</button>The style attribute#
style accepts either a string or a CSSProperties object. Object values use
camelCase keys (like React) and are converted to kebab-case CSS properties.
import { renderToString } from "@vincle/core";
// String styleconst html1 = await renderToString(<p style="color: red; font-size: 1.5rem;">Hello</p>);
// Object style (camelCase → kebab-case conversion)const html2 = await renderToString(<p style={{ color: "red", fontFamily: "sans-serif" }}>Hello</p>);A string is your declaration list, escaped for the attribute and otherwise emitted as written. An object is data: names carrying CSS syntax are dropped and values are CSS-escaped, so a value cannot add a declaration. See the security model.
Event handlers#
A function has no HTML form, and there is no client runtime to attach it to:
any function-valued attribute throws at render time, on* or not. A string
handler (onClick="doThing()") is escaped, but the JavaScript inside is yours:
never interpolate user data there. The
no-unsafe-event-handlers
rule flags the practice at the source.
Custom attributes#
Any name renders as written (data-*, aria-*, hx-get), as long as it is
valid: no whitespace, quote, <, >, /, = or control character.
Where to next#
- First render: your first component
- Security model: the URL allowlist