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.
});