Skip to content
PlaygroundCoreTinker with every input type.

Forms

This page demonstrates each input component in its own tiny app. The pattern is always the same: the input's name matches a parameter of the route the button targets. Edit freely, and reload the page to reset everything.

The full story for all of these demos is in Ask the user for information and Forms and input.

TextBox: a line of text

from drafter import *
from dataclasses import dataclass


@dataclass
class State:
    name: str


@route
def index(state: State) -> Page:
    return Page(state, [
        "Hello, " + state.name + "!\n",
        "Change my name:",
        TextBox("new_name", state.name),
        "\n",
        Button("Rename", "rename")
    ])


@route
def rename(state: State, new_name: str) -> Page:
    state.name = new_name
    return index(state)


start_server(State("stranger"))

TextBox with int: typed conversion

The annotation int converts the text for you. Type 2.5, then type cat, and read the errors you get.

from drafter import *
from dataclasses import dataclass


@dataclass
class State:
    total: int


@route
def index(state: State) -> Page:
    return Page(state, [
        "The jar holds " + str(state.total) + " marbles.\n",
        "Add how many?",
        TextBox("amount", 1),
        "\n",
        Button("Add", "add")
    ])


@route
def add(state: State, amount: int) -> Page:
    state.total = state.total + amount
    return index(state)


start_server(State(10))

CheckBox: a yes or no

When the box is unchecked, the parameter arrives as False rather than being left out.

from drafter import *
from dataclasses import dataclass


@dataclass
class State:
    subscribed: bool


@route
def index(state: State) -> Page:
    return Page(state, [
        "Subscribed: " + str(state.subscribed) + "\n",
        CheckBox("wants_news", state.subscribed),
        " Send me the pigeon newsletter\n",
        Button("Save", "save")
    ])


@route
def save(state: State, wants_news: bool) -> Page:
    state.subscribed = wants_news
    return index(state)


start_server(State(False))

SelectBox: one from a dropdown

Try adding a fourth flavor, then changing the default to something else.

from drafter import *
from dataclasses import dataclass


@dataclass
class State:
    flavor: str


@route
def index(state: State) -> Page:
    return Page(state, [
        "Current scoop: " + state.flavor + "\n",
        SelectBox("choice", ["vanilla", "pistachio", "blackberry"],
                  state.flavor),
        "\n",
        Button("Scoop it", "scoop")
    ])


@route
def scoop(state: State, choice: str) -> Page:
    state.flavor = choice
    return index(state)


start_server(State("vanilla"))

RadioButtonGroup: one, with all options visible

This offers the same kind of choice as a SelectBox, but every option is visible at once.

from drafter import *
from dataclasses import dataclass


@dataclass
class State:
    size: str


@route
def index(state: State) -> Page:
    return Page(state, [
        "Ordered size: " + state.size + "\n",
        RadioButtonGroup("picked", ["small", "medium", "absurd"],
                         state.size),
        "\n",
        Button("Order", "order")
    ])


@route
def order(state: State, picked: str) -> Page:
    state.size = picked
    return index(state)


start_server(State("small"))

RelatedCheckBox: choose several, receive a list

Checkboxes that share a name arrive together as a single list parameter.

from drafter import *
from dataclasses import dataclass


@dataclass
class State:
    packed: list[str]


@route
def index(state: State) -> Page:
    return Page(state, [
        "Packed so far:",
        BulletedList(state.packed),
        RelatedCheckBox("supplies", "rope"),
        " rope\n",
        RelatedCheckBox("supplies", "lantern"),
        " lantern\n",
        RelatedCheckBox("supplies", "snacks"),
        " snacks\n",
        Button("Pack", "pack")
    ])


@route
def pack(state: State, supplies: list[str]) -> Page:
    state.packed = supplies
    return index(state)


start_server(State([]))

TextArea: room to write

from drafter import *
from dataclasses import dataclass


@dataclass
class State:
    note: str


@route
def index(state: State) -> Page:
    return Page(state, [
        "The fridge note says: " + state.note + "\n",
        "Rewrite it:",
        TextArea("new_note", state.note, rows=3),
        "\n",
        Button("Stick it on", "update")
    ])


@route
def update(state: State, new_note: str) -> Page:
    state.note = new_note
    return index(state)


start_server(State("buy more snacks"))

DateInput and TimeInput: calendars and clocks

Date and time fields arrive as text in ISO form (2026-07-27, 13:30). You can annotate the parameters as str and slice out the pieces you need, or see the DateInput reference for richer types.

from drafter import *
from dataclasses import dataclass


@dataclass
class State:
    when: str


@route
def index(state: State) -> Page:
    return Page(state, [
        "The picnic is planned for: " + state.when + "\n",
        "Date:",
        DateInput("day"),
        "\nTime:",
        TimeInput("moment"),
        "\n",
        Button("Plan it", "plan")
    ])


@route
def plan(state: State, day: str, moment: str) -> Page:
    state.when = day + " at " + moment
    return index(state)


start_server(State("not planned yet"))

Where next

  • Big form shows several inputs working together on one page.
  • Lists and tables shows how to display the data that forms collect.