# Interactive Forms URL: /docs/usage/forms/ Section: usage Tags: forms, fields, input, validation -------------------------------------------------------------------------------- Milo includes a built-in form system for collecting structured input. Define fields declaratively with FieldSpec, then run form() to get a dictionary of responses. Quick example from milo import form, FieldSpec, FieldType result = form( FieldSpec("name", "Your name"), FieldSpec("env", "Environment", field_type=FieldType.SELECT, choices=("dev", "staging", "prod")), FieldSpec("confirm", "Deploy?", field_type=FieldType.CONFIRM), ) # result = {"name": "Alice", "env": "prod", "confirm": True} Field types TextDefault Password Select Confirm The default field type. Supports cursor movement (arrows, Home, End), insert, delete, and backspace. FieldSpec("name", "Your name") FieldSpec("email", "Email", placeholder="user@example.com") Same as text, but input is masked. FieldSpec("token", "API Token", field_type=FieldType.PASSWORD) Cycle through choices with Up/Down arrow keys. Renders as a radio-style list with [x] / [ ] indicators. FieldSpec("region", "Region", field_type=FieldType.SELECT, choices=("us-east-1", "eu-west-1", "ap-southeast-1")) Yes/No toggle. Use Y/N keys or Left/Right arrows. FieldSpec("proceed", "Continue?", field_type=FieldType.CONFIRM, default=True) Validation Pass a validator function to FieldSpec. It receives the field value and returns None (valid) or an error message string. def validate_email(value): if "@" not in value: return "Must be a valid email address" return None FieldSpec("email", "Email", validator=validate_email) Warning Warning Validators run on every keystroke for text fields. Keep them fast — avoid network calls or file I/O. For async validation, use a saga on form submission instead. Keyboard navigation Key Action Tab / Shift+Tab Move between fields Enter Submit form (or advance to next field) Up / Down Cycle select choices Left / Right Toggle confirm fields Home / End Jump to start/end of text Ctrl+C Cancel form TTY fallback When stdin is not a TTY (piped input, CI environments), form() falls back to plain input() prompts automatically. No code changes needed. Tip Tip This means forms work in CI pipelines — pipe answers via stdin or set environment variables and read them in your reducer as defaults. Using form_reducer directly For full control, use form_reducer directly in an App instead of the form() helper: from milo import App from milo.form import form_reducer specs = [FieldSpec("name", "Name"), FieldSpec("age", "Age")] app = App(template="form.kida", reducer=form_reducer, initial_state={"specs": specs}) final = app.run() Info When to use form() vs form_reducer Use form() for standalone data collection — it creates its own App, runs it, and returns the result dict. Good for scripts and CLI tools. Use form_reducer when forms are part of a larger app — embed form state in a Flow screen, combine with other reducers, or add custom middleware. -------------------------------------------------------------------------------- Metadata: - Author: lbliii - Word Count: 389 - Reading Time: 2 minutes