Skip to content
Vincle

Loading…

    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 style
    const 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#