Skip to content

Compose built-ins

Hedron’s built-ins share one rendering model: layout components arrange children, semantic components describe the document, surfaces group a bounded task, and controls submit to or replace an explicitly addressable region. You can nest any NodeLike component without manually rendering it or converting it to HTML first.

Integrated example

Team settings

Profile

Layouts, surfaces, fields, controls, and status content keep one visual rhythm.

Workspace detailsSaved
Ready to save.
Deployment completed successfully.

The example is ordinary semantic HTML. The tabs and form response are enhanced by the same documentation JavaScript used on the individual demos, while a real application returns the replacement fragment from its route.

Build from semantic ownership outward

Start with the page landmarks, then choose the smallest component that owns each relationship:

from hedron import (
    Card,
    Container,
    Form,
    FormField,
    Grid,
    Heading,
    Main,
    Sidebar,
    Stack,
    SubmitButton,
    Tabs,
    Text,
    TextInput,
)

workspace = Container(
    Grid(
        Sidebar(Text("Workspace navigation"), label="Workspace"),
        Main(
            Stack(
                Heading("Team settings", level=1),
                Card(
                    Form(
                        FormField(
                            name="team_name",
                            label="Team name",
                            control=TextInput("team_name"),
                        ),
                        SubmitButton("Save changes"),
                        action="/settings/team",
                    ),
                    title="Workspace details",
                    id="workspace-details",
                ),
                Tabs(
                    ("Activity", Text("Deployment completed successfully.")),
                    ("Members", Text("12 active members")),
                ),
            )
        ),
        columns=2,
    ),
    id="team-workspace",
)

Container, Stack, Inline, Grid, Card, Form, Fragment, landmarks, Dialog, Expander, and Sidebar accept positional nodes or a children= value. Use positional children for a small, readable tree and children= when a typed model or loop already produced a sequence. If both are supplied, positional nodes render first. Strings are always one text node; they are never split as a sequence.

Nest components without crossing the renderer boundary

Pass a child component itself, not child.render() and not render(child).html. Keeping the component in the tree preserves slot validation, cycle and depth checks, escaping, diagnostics, and request-local identity tracking. FormField follows this rule too: it copies and binds a compatible control, then lets the normal renderer process that bound component.

Use ordinary children for the component’s main content. Use named slots only for a secondary relationship whose placement belongs to the parent, such as a Card header or footer, Dialog actions, or Sidebar body. Constructor slot arguments are concise; the fluent .slot() form is useful when a builder assembles the same component in stages.

Make replacement boundaries explicit

Put a stable id on the smallest complete region that a request replaces. Layout and surface IDs are intentionally ordinary DOM IDs, so an HTMX target, an anchor, a browser test, and an accessibility relationship all refer to the same node.

from hedron import Card, ComponentRef, RefreshButton, Text

activity_ref = ComponentRef(
    logical_id="activity.latest",
    path="/activity/latest",
    target="#activity",
    swap="outerHTML",
)

activity = Card(
    Text("Deployment completed successfully."),
    title="Latest activity",
    id="activity",
    footer=RefreshButton("Refresh", ref=activity_ref),
)

The /activity/latest response must return the same outer boundary—including id="activity"—when using outerHTML. For innerHTML, return only the region’s children. Loading, empty, validation, success, and recoverable-error responses should all preserve the target contract. Keep GET requests safe; use POST plus authentication, authorization, CSRF protection, and server-side validation for mutation.

IDs and nested relationships

Supply an explicit id when external code needs a durable name. When an ID exists only to connect internal elements, let the component generate it. Tabs prefixes every tab and panel relationship with a request-local component ID, so sibling and nested tab sets cannot collide. FormField does the same for its label, control, help, and error nodes; pass id= only when another component or test must address that exact field.

Application classes augment built-in theme classes. For example, Stack(class_="settings-flow") emits both hedron-stack and settings-flow, retaining the default behavior while giving your theme a stable extension point. The validated gap on Stack, Inline, and Grid is applied by the shipped stylesheet without an inline style, so strict CSP remains intact.

Composition checklist

  • Choose a native landmark or content component before a generic layout wrapper.
  • Keep source order equal to reading, focus, and mobile order.
  • Replace the smallest complete region and preserve its target ID in every response.
  • Pass components through the renderer; never splice rendered HTML strings together.
  • Use a parent slot for parent-owned placement and children for the primary flow.
  • Give repeated landmarks distinct accessible labels.
  • Render representative full trees in tests, then test HTMX methods, targets, swaps, validation, focus, and announcements in a browser.

All component demos · HTMX interactions · Test your UI