UI reference

Form Field Types Reference

Complete reference for every form descriptor class in core.forms.

All imports:

from core.forms import Form, Field, Lookup, RadioCards, Card, Section

Form

Base class for all form definitions. Subclass it and set the class attributes.

class MyForm(Form):
    schema = MySchema          # pytastic TypedDict
    title = "Create Thing"     # Drawer heading
    action = "/api/things"     # Submit endpoint
    method = "POST"            # HTTP method
    submit_label = "Create"    # Submit button text
    fields = [...]             # List of descriptors
Attribute Type Default Description
schema type[TypedDict] required pytastic schema for validation
title str "" Drawer heading text
action str "" API endpoint URL
method str "POST" HTTP method (POST, PUT, PATCH)
submit_label str "Create" Submit button label
fields list [] Ordered list of field descriptors

Methods

Form.validate(data, **kwargs) -> dict

Validates data against the schema with strip=True (extra fields removed). Raises pytastic.ValidationError on failure.

from pytastic import ValidationError

try:
    validated = MyForm.validate({"name": "x"})
except ValidationError as e:
    print(e.errors)  # [{"path": "name", "message": "Min length 3"}]

Form.field_meta() -> dict

Returns per-field metadata introspected from the schema:

MyForm.field_meta()
# {
#     "name": {"required": True, "base_type": "str", "min_len": "3"},
#     "status": {"required": True, "base_type": "literal", "choices": ["active", "archived"]},
# }

Field

A single form input. The most common descriptor.

Field(name, type="auto", label=None, placeholder="",
      autofocus=False, options=None, help_text="")
Parameter Type Default Description
name str required Must match a key in the schema
type str "auto" Input type (see table below)
label str | None None Human label. Auto-generated from name if None
placeholder str "" Input placeholder text
autofocus bool False Focus this field when the form opens
options list[dict] | None None For type="select": [{"value": "x", "label": "X"}]
help_text str "" Small text below the input

Supported Types

Type Renders As Auto-Detected When
"text" <input type="text"> Schema type is str
"textarea" <textarea> Never (must be explicit)
"number" <input type="number"> Schema type is int or float
"email" <input type="email"> Schema has format=email
"date" <input type="date"> Never (must be explicit)
"datetime-local" <input type="datetime-local"> Schema has format=date-time
"select" <select> Schema type is Literal[...]
"hidden" <input type="hidden"> Never (must be explicit)
"checkbox" <input type="checkbox"> Schema type is bool
"auto" (inferred) Default — resolved at render time

HTML Attributes from Schema

These pytastic constraints are automatically applied as HTML attributes:

Schema Constraint HTML Attribute
min_len / min_length minlength
max_len / max_length maxlength
min min
max max
step step

Examples

# Simple text
Field("name", label="Full Name", placeholder="Jane Smith")

# Textarea with help text
Field("bio", type="textarea", help_text="Markdown supported")

# Number with schema constraints (min/max applied automatically)
Field("quantity")  # Schema: Annotated[int, "min=1; max=100"]

# Select with explicit options
Field("color", type="select", options=[
    {"value": "red", "label": "Red"},
    {"value": "blue", "label": "Blue"},
])

# Select auto-detected from Literal schema
Field("status")  # Schema: Literal["active", "archived"]
# Renders as <select> with options Active, Archived

Lookup

API-driven dropdown with search. Fetches results as the user types.

Lookup(name, endpoint, display, value, label=None,
       search_param="q", placeholder="Search...",
       empty_label=None, multi=False, debounce_ms=300)
Parameter Type Default Description
name str required Schema field name
endpoint str required GET URL returning JSON array
display str required Format string for labels: "{name}", "{first} {last}"
value str required Column name to save as the field value
label str | None None Human label
search_param str "q" Query parameter name for filtering
placeholder str "Search..." Input placeholder
empty_label str | None None Text when nothing is selected
multi bool False Allow multiple selections
debounce_ms int 300 Debounce delay in milliseconds

API Response Format

The endpoint should return one of these shapes:

// Plain array (preferred)
[{"id": "1", "name": "Alice"}, {"id": "2", "name": "Bob"}]

// Wrapped in "results"
{"results": [{"id": "1", "name": "Alice"}]}

// Wrapped in "data"
{"data": [{"id": "1", "name": "Alice"}]}

Examples

# Simple lookup
Lookup("team_id",
    endpoint="/api/teams",
    display="{name}",
    value="id",
    placeholder="Search teams...")

# Multi-column display
Lookup("contact_id",
    endpoint="/api/contacts",
    display="{name} ({email})",
    value="id")

RadioCards

Horizontal selectable cards rendered as radio inputs. Best for 2–5 options where each needs a label and description.

RadioCards(name, cards=[], label=None, default=None)
Parameter Type Default Description
name str required Schema field name
cards list[Card] [] Card definitions
label str | None None Group label
default str | None None Pre-selected card value

Card

One option inside a RadioCards group.

Card(value, label, description="", icon="", shows=None)
Parameter Type Default Description
value str required The value submitted when selected
label str required Card heading
description str "" Small text below the label
icon str "" Lucide icon name
shows str | None None Documentation hint: section key revealed by this card

Example

RadioCards("plan", label="Choose Plan", default="free", cards=[
    Card("free",   label="Free",   icon="gift",    description="Up to 3 projects"),
    Card("pro",    label="Pro",    icon="zap",     description="Unlimited projects"),
    Card("team",   label="Team",   icon="users",   description="Team management",
         shows="team_config"),
])

Section

Groups fields visually. Supports conditional visibility and collapsibility.

Section(key, fields=[], label=None, depends_on=None,
        collapsible=False, collapsed=False)
Parameter Type Default Description
key str required Unique identifier for this section
fields list [] Child descriptors (Field, Lookup, RadioCards, Section)
label str | None None Section heading (uppercase, small)
depends_on dict | None None {"field": "name", "value": "val"} — show only when condition is met
collapsible bool False Allow toggling open/closed
collapsed bool False Start collapsed (only if collapsible=True)

Dependent Sections

Use depends_on to conditionally show a section based on another field's value:

Section("team_config", label="Team Settings",
        depends_on={"field": "plan", "value": "team"},
        fields=[
    Field("team_name", label="Team Name"),
    Lookup("admin_id", label="Team Admin", ...),
])

This section is hidden until plan equals "team". The Alpine.js x-show directive handles the reactivity.

Nested Sections

Sections can contain other sections for complex form layouts:

Section("outer", label="Outer", fields=[
    Field("field_a"),
    Section("inner", label="Inner", depends_on={...}, fields=[
        Field("field_b"),
    ]),
])

Collapsible Sections

Section("advanced", label="Advanced",
        collapsible=True, collapsed=True,
        fields=[...])

Renders with a clickable legend and a chevron that rotates when expanded.


Template Integration

Required Includes

Every page that uses forms needs these:

<!-- Alpine.js form components (once per page) -->
{% include "forms/partials/scripts.html" %}

<!-- Target container (once per page) -->
<div id="form-panel"></div>

Trigger Buttons

Load a form with any HTMX trigger:

<!-- Button -->
<button hx-get="/partials/forms/new-task"
        hx-target="#form-panel"
        hx-swap="innerHTML">
  New Task
</button>

<!-- Link -->
<a hx-get="/partials/forms/edit-board"
   hx-target="#form-panel"
   hx-swap="innerHTML">
  Edit
</a>

Success Events

After a successful submission, the form dispatches a form:success event:

window.addEventListener('form:success', (e) => {
  const { action, method, data } = e.detail;
  // Refresh a list, redirect, show a toast, etc.
});