Form state and validation (FormState, Validators) are part of tamboui-widgets and work at any API level. The formField() and form() builders shown here are Toolkit DSL conveniences—see Toolkit DSL for an introduction.

TamboUI provides high-level form abstractions to simplify building interactive forms with labels, validation, and centralized state management.

Overview

Building forms traditionally requires significant boilerplate:

  • Individual state declarations for each field

  • Manual label + input pairing

  • Repetitive styling

  • Custom helper methods

The form abstractions reduce this to concise, declarative code:

// Centralized state for all form fields
FormState form = FormState.builder()
    .textField("fullName", "Ada Lovelace")
    .textField("email", "ada@example.com")
    .booleanField("subscribe", true)
    .selectField("country", Arrays.asList("USA", "UK", "Germany"), 0)
    .build();

// Simple field declarations
Element overviewExample = formField("Full Name", form.textField("fullName"))
    .labelWidth(12)
    .rounded()
    .borderColor(Color.DARK_GRAY)
    .focusedBorderColor(Color.CYAN);

Form Fields

The formField() factory creates a labeled input field that pairs a label with an input widget.

Basic Usage

import static dev.tamboui.toolkit.Toolkit.*;

        // With TextInputState
        TextInputState nameState = new TextInputState("John");
        formField("Name", nameState);

        // Creates internal state automatically
        formField("Email")
            .placeholder("you@example.com");

Field Types

FormFieldElement supports multiple input types:

Type Description State Class

TEXT (default)

Single-line text input

TextInputState

CHECKBOX

Boolean checkbox with [x] rendering

BooleanFieldState

TOGGLE

On/off toggle switch

BooleanFieldState

SELECT

Dropdown selection

SelectFieldState

Text Fields

TextInputState emailState = new TextInputState("");
formField("Email", emailState)
    .placeholder("you@example.com")
    .rounded();

Password Fields

Use maskedField() in the FormState builder for password fields:

FormState form = FormState.builder()
    .textField("username", "")
    .maskedField("password", "")  // automatically masked with '*'
    .build();

When used with FormElement, masking is applied automatically. For formField(), you can use .masked() explicitly:

formField("Password", passwordState)
    .placeholder("Enter password")
    .masked()          // displays '*' for each character
    .rounded();

// Or with a custom mask character
formField("PIN", pinState)
    .masked('\u25CF');       // displays a filled circle for each character

The actual text is stored in the state; only the display is masked.

Boolean Fields (Checkbox/Toggle)

BooleanFieldState subscribeState = new BooleanFieldState(false);

// Checkbox style: [x] or [ ]
formField("Subscribe", subscribeState);

// Toggle style: [ON] or [OFF]
formField("Dark Mode", subscribeState, FieldType.TOGGLE);

Select Fields

SelectFieldState countryState = new SelectFieldState("USA", "UK", "Germany");
formField("Country", countryState);

Styling

FormFieldElement supports comprehensive styling options:

formField("Email", emailState)
    .labelWidth(14)           // Fixed label width for alignment
    .spacing(2)               // Gap between label and input
    .rounded()                // Rounded border
    .borderColor(Color.DARK_GRAY)
    .focusedBorderColor(Color.CYAN)
    .errorBorderColor(Color.RED);

CSS Selectors

FormFieldElement exposes these CSS child selectors:

Selector Description

FormFieldElement-label

The label text style

FormFieldElement-input

The input wrapper

FormFieldElement-error

The inline error message style

Example TCSS:

FormFieldElement-label {
    color: gray;
}

FormFieldElement-error {
    color: red;
}

Form State

FormState provides centralized state management for all form fields, eliminating the need for individual state declarations.

Creating Form State

FormState form = FormState.builder()
    // Text fields
    .textField("username", "")
    .textField("email", "user@example.com")

    // Boolean fields
    .booleanField("newsletter", true)
    .booleanField("darkMode", false)

    // Select fields
    .selectField("country", Arrays.asList("USA", "UK", "Germany"), 0)
    .selectField("role", Arrays.asList("Admin", "User", "Guest"))  // defaults to index 0

    .build();

Accessing Values

// Get state objects for UI binding
TextInputState usernameState = form.textField("username");
BooleanFieldState newsletterState = form.booleanField("newsletter");
SelectFieldState countryState = form.selectField("country");

// Get/set text values directly
String email = form.textValue("email");
form.setTextValue("email", "new@example.com");

// Get/set boolean values
boolean subscribed = form.booleanValue("newsletter");
form.setBooleanValue("newsletter", false);

// Get/set select values
String country = form.selectValue("country");
int countryIndex = form.selectIndex("country");
form.selectIndex("country", 2);  // Select "Germany"

// Get all text values as map
Map<String, String> allText = form.textValues();

Validation

The validation framework provides built-in validators and supports custom validation logic.

Built-in Validators

Validator Description Example

required()

Field must not be empty

Validators.required()

email()

Valid email format

Validators.email()

minLength(n)

Minimum n characters

Validators.minLength(3)

maxLength(n)

Maximum n characters

Validators.maxLength(100)

pattern(regex)

Matches regex pattern

Validators.pattern("\\d{5}")

range(min, max)

Numeric value in range

Validators.range(1, 100)

Using Validators

formField("Email", emailState)
    .validate(Validators.required(), Validators.email())
    .errorBorderColor(Color.RED)
    .showInlineErrors(true);

Custom Error Messages

All built-in validators accept an optional custom message:

Validators.required("Please enter your name");
Validators.email("Invalid email format");
Validators.minLength(3, "Username must be at least 3 characters");
Validators.range(1, 100, "Age must be between 1 and 100");

Triggering Validation

FormFieldElement field = formField("Email", emailState)
    .validate(Validators.required(), Validators.email());

// Validate and get result
ValidationResult result = field.validateField();

if (!result.isValid()) {
    String error = result.errorMessage();  // "Field is required" or "Invalid email"
}

// Get last validation result
ValidationResult lastResult = field.lastValidation();

Custom Validators

Create custom validators using the Validator functional interface:

Validator usernameAvailable = value -> {
    if (userService.exists(value)) {
        return ValidationResult.invalid("Username already taken");
    }
    return ValidationResult.valid();
};

formField("Username", usernameState)
    .validate(Validators.required(), usernameAvailable);

Composing Validators

Validators can be composed using and():

Validator emailValidator = Validators.required()
    .and(Validators.email())
    .and(Validators.maxLength(100));

formField("Email", emailState)
    .validate(emailValidator);

Form Container

FormElement provides a container for managing multiple form fields with optional grouping.

Basic Usage

form(formState)
    .field("fullName", "Full Name")
    .field("email", "Email")
    .field("role", "Role")
    .labelWidth(14)
    .rounded();

Grouping Fields

form(formState)
    .group("Personal Info")
        .field("fullName", "Full Name")
        .field("email", "Email")
        .field("phone", "Phone")
    .group("Preferences")
        .field("newsletter", "Newsletter", FieldType.CHECKBOX)
        .field("darkMode", "Dark Mode", FieldType.TOGGLE)
    .labelWidth(14)
    .spacing(1);

Form Submission

FormElement supports two submission patterns:

Submit on Enter

When enabled, pressing Enter in any text field triggers form submission:

form(formState)
    .field("username", "Username")
    .field("password", "Password")
    .submitOnEnter(true)
    .onSubmit(state -> {
        String user = state.textValue("username");
        String pass = state.textValue("password");
        authenticate(user, pass);
    });

Programmatic Submit (Button)

Call submit() from a button or other trigger:

FormElement loginForm = form(formState)
    .field("username", "Username", Validators.required())
    .field("password", "Password", Validators.required())
    .onSubmit(state -> authenticate(state));

// Render form with submit button
column(
    loginForm,
    text(" Login ").bold()  // Note: button() is not yet available
);

Validation on Submit

By default, submit() validates all fields before calling the onSubmit callback.

If validation fails, the onSubmit callback is not called and submit() returns false.
FormElement form = form(formState)
    .field("email", "Email", Validators.required(), Validators.email())
    .validateOnSubmit(true)  // default behavior
    .onSubmit(state -> {
        // This is only called if ALL validations pass
        save(state);
    });

boolean success = form.submit();
// success == true  -> validation passed, onSubmit was called
// success == false -> validation failed, onSubmit was NOT called

To always call onSubmit regardless of validation:

form(formState)
    .validateOnSubmit(false)  // skip validation
    .onSubmit(state -> save(state));  // always called

Arrow Key Navigation

By default, Up/Down arrow keys only work in select fields (to change selection). Enable arrowNavigation to allow Up/Down to move between fields:

form(formState)
    .field("username", "Username")
    .field("email", "Email")
    .field("role", "Role", FieldType.SELECT)
    .arrowNavigation(true);  // Up/Down navigate between fields

When enabled:

  • Text fields: Up/Down navigate to previous/next field (like Tab/Shift+Tab)

  • Boolean fields (checkbox, toggle): Up/Down navigate to previous/next field

  • Select fields: Up/Down still change the selection (unchanged behavior)

Complete Example

Here’s a complete form example using all the abstractions:

public static class SettingsForm {

    private static final FormState FORM = FormState.builder()
        // Profile
        .textField("fullName", "Ada Lovelace")
        .textField("email", "ada@analytical.io")
        .textField("role", "Research")
        .textField("timezone", "UTC+1")
        // Preferences
        .textField("theme", "Nord")
        .booleanField("notifications", true)
        // Security
        .textField("twoFa", "Enabled")
        .build();

    public static Element render() {
        return column(
            panel("Profile", column(
                formField("Full name", FORM.textField("fullName"))
                    .labelWidth(14).rounded()
                    .borderColor(Color.DARK_GRAY)
                    .focusedBorderColor(Color.CYAN),
                formField("Email", FORM.textField("email"))
                    .labelWidth(14).rounded()
                    .borderColor(Color.DARK_GRAY)
                    .focusedBorderColor(Color.CYAN)
                    .validate(Validators.required(), Validators.email()),
                formField("Role", FORM.textField("role"))
                    .labelWidth(14).rounded()
                    .borderColor(Color.DARK_GRAY)
                    .focusedBorderColor(Color.CYAN)
            ).spacing(1)).rounded().borderColor(Color.CYAN),

            panel("Preferences", column(
                formField("Theme", FORM.textField("theme"))
                    .labelWidth(14).rounded()
                    .borderColor(Color.DARK_GRAY),
                formField("Notifications", FORM.booleanField("notifications"))
                    .labelWidth(14)
            ).spacing(1)).rounded().borderColor(Color.GREEN),

            row(
                text(" Save ").bold().black().onGreen(),
                text(" Cancel ").bold().white().bg(Color.DARK_GRAY)
            ).spacing(2)
        ).spacing(1).fill();
    }
}

State Classes Reference

TextInputState

TextInputState state = new TextInputState("initial");

// Get/set text
String text = state.text();
state.setText("new value");

// Cursor operations
state.insert('c');
state.deleteBackward();
state.deleteForward();
state.moveCursorLeft();
state.moveCursorRight();
state.clear();

BooleanFieldState

BooleanFieldState state = new BooleanFieldState(false);

// Get/set value
boolean value = state.value();
state.setValue(true);

// Toggle
state.toggle();  // Flips the value

SelectFieldState

SelectFieldState state = new SelectFieldState("A", "B", "C");
// Or with List
SelectFieldState stateFromList = new SelectFieldState(options, 1);  // Select index 1

// Get values
String selected = state.selectedValue();  // "A"
int index = state.selectedIndex();        // 0
List<String> opts = state.options();   // ["A", "B", "C"]

// Change selection
state.selectIndex(2);    // Select "C"
state.selectNext();      // Move to next option (wraps)
state.selectPrevious();  // Move to previous option (wraps)

Migration Guide

To migrate from manual form construction to the new abstractions:

Before

// Individual state declarations
private static final TextInputState FULL_NAME = new TextInputState("Ada");
private static final TextInputState EMAIL = new TextInputState("ada@example.com");
private static final TextInputState ROLE = new TextInputState("Research");

// Custom helper
private static Element formRow(String label, TextInputState state) {
    return row(
        text(label).dim().length(14),
        textInput(state).rounded().borderColor(Color.DARK_GRAY).fill()
    ).spacing(1).length(3);
}

// Usage
Element beforeExample1 = formRow("Full name", FULL_NAME);
Element beforeExample2 = formRow("Email", EMAIL);

After

// Centralized state
private static final FormState FORM_STATE = FormState.builder()
    .textField("fullName", "Ada")
    .textField("email", "ada@example.com")
    .textField("role", "Research")
    .build();

// Usage - no helper needed
Element afterExample1 = formField("Full name", FORM_STATE.textField("fullName"))
    .labelWidth(14).rounded().borderColor(Color.DARK_GRAY);
Element afterExample2 = formField("Email", FORM_STATE.textField("email"))
    .labelWidth(14).rounded().borderColor(Color.DARK_GRAY);