Skip to content

textual-wtf

Declarative, validated forms for Textual TUI applications.

textual-wtf brings the ergonomics of Django forms to the terminal. Define your form fields as class attributes, compose them into a layout in one line, and get real-time validation, embedded sub-forms, and tabbed multi-form support — all without writing boilerplate widget code.

  • Declarative API


    Define fields as class attributes on a Form subclass. Field order is preserved. No metaclass magic to wrestle with — it just works.

    class SignupForm(Form):
        username = StringField("Username", required=True)
        email    = StringField("Email", validators=[EmailValidator()])
        age      = IntegerField("Age", minimum=18)
    
  • Three-level required cascade


    Control required state at the field, class, instance, or render level. Each level overrides the previous, giving you fine-grained control when embedding sub-forms.

    Required cascade

  • Embedded sub-forms


    Assign a Form instance as a class attribute. Its fields are flattened into the parent with a name prefix (billing_street, billing_city, …). Required state cascades independently per embedding.

    Embedding guide

  • Tabbed multi-form layout


    TabbedForm renders multiple forms as tabs. Tab labels turn red whenever any field in that tab has a validation error, giving the user instant visual feedback.

    Tabbed forms

  • Real-time validation


    Validators fire on the right events automatically. MaxLength fires on every keystroke; Required fires on blur and submit. Custom validators are one function or one class away.

    Validation guide

  • Flexible rendering


    Use layout() for a zero-config layout, simple_layout() for full chrome on each field, or bf() for the raw widget when you need complete layout freedom.

    Layout guide

At a glance

contact_form.py
from textual.app import App, ComposeResult, on
from textual_wtf import Form, StringField, IntegerField, BooleanField
from textual_wtf import EmailValidator

class ContactForm(Form):
    title = "Contact Us"

    name    = StringField("Name", required=True, min_length=2)
    email   = StringField("Email", required=True, validators=[EmailValidator()])
    age     = IntegerField("Age", minimum=0, maximum=120)
    updates = BooleanField("Subscribe to updates")


class ContactApp(App):
    def compose(self) -> ComposeResult:
        self.form = ContactForm()
        yield self.form.layout()

    @on(ContactForm.Submitted)
    def on_submitted(self, event: ContactForm.Submitted) -> None:
        data = event.form.get_data()
        self.notify(f"Submitted: {data}")

    @on(ContactForm.Cancelled)
    def on_cancelled(self, event: ContactForm.Cancelled) -> None:
        self.app.exit()


if __name__ == "__main__":
    ContactApp().run()

Installation

uv add textual-wtf
pip install textual-wtf
poetry add textual-wtf

Python version

textual-wtf requires Python 3.11 or later and Textual 1.0.0 or later.

Where to go next

Resource What you'll find
Getting Started Step-by-step first form, submit/cancel handling
Guide: Forms Class attributes, required cascade, data access
Guide: Fields All field types and validators
Guide: Layout Rendering modes and custom layouts
Guide: Embedding Sub-form embedding and field prefixes
Guide: Validation When validation fires, cross-field logic
Guide: Tabbed Forms Multi-form tab layouts
API Reference Complete API documentation