Skip to content

Usage

Overview

reactpy.html is how you write markup in ReactPy. It is an object whose attributes are tag constructors: html.div(...) builds a <div>, and there is no template language, no JSX, and no parser involved.

from reactpy import html

html.div({"class": "card"}, html.h2("Title"), html.p("Body"))

That call produces a plain dict:

{
    "children": [
        {"children": ["Title"], "tagName": "h2"},
        {"children": ["Body"], "tagName": "p"},
    ],
    "attributes": {"class": "card"},
    "tagName": "div",
}

reactpy.h is the same object under a shorter name — h is html is True — so use whichever reads better in a given file. Importing both and mixing them in one module buys nothing.


Reference

html.<tag_name>(attributes, *children)

Every attribute of html is a tag constructor. The common tags are declared for autocomplete, but any name works — see Any name is a tag.

  • First argument: the attributes — but only when it is a plain dict. A VdomDict, or any dict subclass, is treated as a child instead. This single rule is the most common source of confusion, and it is worth internalizing:

    html.div({"class": "card"}, html.p("Body"))  # dict -> attributes
    html.div(html.p("Body"))  # no dict -> no attributes
    html.div(None, html.p("Body"))  # None -> an explicit "no attributes"
    

    Leading None is the conventional way to say "no attributes" and keep the arguments in a predictable shape. It renders nothing.

    A VdomDict as the first argument is a child

    VdomDict is a dict subclass, and the check is for the exact plain dict type. So a VdomDict is never mistaken for attributes — even when you meant it that way:

    from reactpy.types import VdomDict
    
    html.div(VdomDict(tagName="span"))  # -> a <span> *child* of the div
    
  • Remaining arguments: children, of any type Common Props accepts — strings, numbers, None, lists, dicts, components. Lists are flattened.

  • Returns: a VdomDict. Tag constructors are cached, so html.div is html.div — the same object every time. Nothing is rendered until a component returns it.

Caveats

  • A plain dict that is meant as an element can be read as attributes. html.div({"children": ["x"]}) produces a div with an attribute literally named children, not a child — because the first argument was a plain dict. When in doubt, pass the element as the second argument after None.
  • Void tags reject children. html.br("x") raises TypeError: 'br' nodes cannot have children. See Common Props.
  • Tags do not validate their own attributes. html.img({"scr": "typo.png"}) is accepted and renders an image with no source. There is no prop-type check; a misspelled attribute name is a silent no-op.

html(*children) — fragments

Calling html itself is the fragment constructor: children with no wrapper element. See Common Props — a fragment accepts key and nothing else.


Usage

Any name is a tag

ReactPy does not keep a list of valid HTML tags. It converts the attribute name and sends it as the tag name, so you can reach tags the module has never declared:

You write You get
html.div <div>
html.data_table <data-table>
html.my_widget <my-widget>
html.del_ <del>
html.class_ <class>

Two transformations: a trailing underscore is stripped (so you can name tags that are Python keywords, like del and class), and remaining underscores become dashes (so you can name custom elements and hyphenated tags that are not valid Python identifiers).

That makes custom elements first-class — no registration step, no wrapper component:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# Any name is a tag: underscores become dashes, so this is <user-badge>.
user_badge = html.user_badge


@component
def profile(name, role):
    return html.section(
        {"class": "profile", "data-role": role},
        html.h2(name),
        # A plain string is a text child; an attribute dict is always first.
        html.p({"class": "muted"}, "Signed in as ", html.strong(name)),
        user_badge({"role": role, "label": role.title()}),
        html.footer(
            html.small(
                {"style": {"font-size": "12px", "color": "#666"}},
                "Last seen today",
            )
        ),
    )


@component
def app():
    return html.main(
        None,
        html.h1("Accounts"),
        profile("Ada", "admin"),
        profile("Linus", "viewer"),
    )
1
# TODO

The <user-badge> element reaches the browser as written. Whether it does anything is up to whatever defines it — a web component, an external script, or nothing at all. A tag the browser does not know renders as an unknown element: its children show up, its attributes are set, and it is laid out inline by default.

Prefer a component over a mystery tag

A custom tag is only worth it when something on the client implements it. For grouping your own markup, write a @component — it takes part in rendering, can hold state, and is checkable by a type checker.

Tags versus components

Both are called the same way and both produce VDOM, and the difference is what happens next:

html.div(...) my_component(...)
Returns A VdomDict, immediately A Component, rendered later by the layout
Owns state No Yes — hooks belong to it
Re-rendered Only when its parent returns a new value When its state changes or its parent re-renders it
Defined with Provided by html @component over a function

The practical rule: a tag is for HTML elements, a component is for anything with behaviour. A <div> that has to remember something is not a div any more — it is a component that returns a div. See Your First Component.

Nested and adjacent tags

Tags nest by call nesting, and read left-to-right, innermost first:

html.p(html.em("emphasized"), " and ", html.strong("bold"))

Because Python evaluates arguments eagerly, the inner call completes before the outer one runs. There is no indentation-scope or closing-tag syntax to get wrong, and no unbalanced-tag error — the structure either parses or it does not.

The one habit to build is the leading None. Without it, a first child that is a plain dict is consumed as attributes:

html.div({"tagName": "span"})  # attributes named "tagName" — not a child
html.div(None, {"tagName": "span"})  # a <span> child

A complete page reads like the HTML it produces:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@component
def article_card():
    # Children are positional. The first argument is attributes only if it is a
    # plain dict, which is why a leading `None` is needed when there are no
    # attributes but the first child could look like one.
    return html.article(
        {"class": "card"},
        html.h2("Naming rules"),
        html.p(
            html.em("html.del_"),
            " is ",
            html.code("<del>"),
            ", ",
            html.code("html.data_table"),
            " is ",
            html.code("<data-table>"),
            ".",
        ),
        html.ul(
            None,
            [html.li({"key": word}, word) for word in ("class", "style", "key")],
        ),
        html.hr(),
        html.p({"class": "muted"}, "Void tags take no children."),
    )


@component
def app():
    return html.main(None, article_card(), html.br(), html.p("End of page."))
1
# TODO

Lists of tags

A list comprehension produces children directly; give each one a key:

html.ul(None, [html.li({"key": word}, word) for word in words])

See Rendering Lists for why the key matters, and Common Props for where it lives in the dictionary.

Classes and inline styles

class and className both work, and style takes a dict. Include units inside the strings — a bare number is applied by some executors and silently dropped by others:

html.p({"class": "muted"}, "...")
html.p({"style": {"font-size": "12px", "color": "#666"}}, "...")

Details, including the executor difference, are in Common Props.

SVG

SVG elements live under html.svg, because their tag names collide with HTML ones (<title>, <a>, <style>):

html.svg(
    {"viewBox": "0 0 24 24", "width": "24"},
    html.svg.circle({"cx": "12", "cy": "12", "r": "10"}),
)

html.svg is itself a tag constructor, and nested names follow the same rules — trailing underscore stripped, underscores to dashes. Camel-cased SVG names are used as written, so html.svg.clipPath produces <clipPath>.

Names are not case-normalized

viewBox works because the name is forwarded exactly. Write SVG attribute names the way the spec spells them, capitals included.

Raw HTML

Two different jobs get confused here. To put a raw HTML string into the page as-is, without ReactPy parsing it, use dangerouslySetInnerHTML:

html.div({"dangerouslySetInnerHTML": {"__html": "<b>bold</b>"}})

To hand ReactPy a block of HTML and have it parsed into VDOM — real elements you can then inspect and transform — use string_to_reactpy() instead, which is exported at the top of the package:

from reactpy import string_to_reactpy

string_to_reactpy("<b>bold</b>")
# {'children': ['bold'], 'tagName': 'b'}

See Convert Between VDOM and HTML for the parse direction in full, including the *transforms argument.

Never pass untrusted input to either one: dangerouslySetInnerHTML is inserted without sanitizing, and string_to_reactpy() parses whatever you give it. Note also that a dangerouslySetInnerHTML attribute suppresses children rather than raising — nothing checks the combination, the raw HTML simply wins and the children are dropped. See Common Props.

Inspecting what a tag produced

A tag call is a plain dict, so print it:

>>> from reactpy import html
>>> html.li({"key": "a"}, "Item")
{'children': ['Item'], 'attributes': {'key': 'a'}, 'tagName': 'li'}

Key order can differ, and empty keys are omitted — a tag with no attributes has no "attributes" entry at all. To see what a component renders, drive it through reactpy.core.layout.Layout rather than printing the component instance, which shows only its arguments.

Script tags

html.script is special-cased. It rejects event handlers:

html.script({"onLoad": handler})
ValueError: 'script' elements do not support event handlers

and it infers its own key from its content, falling back to src, so the script is not re-run on every render:

>>> html.script({"src": "/analytics.js"})
{'tagName': 'script', 'attributes': {'src': '/analytics.js', 'key': '/analytics.js'}}

Because the key drives re-execution, giving a script an explicit key is how you make it run again on purpose. As with dangerouslySetInnerHTML, keep untrusted data out of the body of a script.


Troubleshooting

The first argument became an attribute

If your element has an attribute named children or tagName and renders nothing, a plain dict child was read as the attributes dict. Pass None first.

TypeError: '<tag>' nodes cannot have children.

You passed content to a void tag. Remove the children.

TypeError: Fragments cannot have attributes besides 'key'

Give the fragment a real element, or move the attributes onto a child.

An attribute has no effect

The name is forwarded verbatim and never validated, so a typo is invisible. Print the dict and compare the key against what the browser expects. Only class/className and for/htmlFor have aliases; nothing else does.

A numeric style does nothing

Add the unit to the string. See Common Props.


At a glance

Tag names

You write You get Rule
html.div <div> as-is
html.data_table <data-table> _ → -
html.del_ <del> trailing _ stripped
html.class_ <class> trailing _ stripped
html.svg.clipPath <clipPath> as-is, under svg

Arguments

Position Type Meaning
first plain dict attributes, or pass None for none
first (otherwise) any accepted child a child
rest any accepted child children, flattened

Accepted children

Child Renders as
str text, verbatim
int / float str(value)
None nothing
True / False the words True / False
list / tuple flattened children
dict an element (needs "tagName")
@component instance that component's output
Authors: Archmonger