Skip to content

FormGroup

The FormGroup component wraps form inputs with labels, help text, and validation feedback in a single, clean component. It eliminates the boilerplate of manually creating Bootstrap form structures and handles validation states automatically.

Goal

By the end of this guide, you'll be able to create professional form fields with validation, help text, and required indicators in a single line of Python.


Quick Start

Here's the simplest way to create a form field.

Live Preview
We'll never share your email
FormGroup(
    Input("email", input_type="email", placeholder="you@example.com"),
    label="Email Address",
    help_text="We'll never share your email"
)

Visual Examples & Use Cases

1. Validation States

Show users when their input is valid or invalid.

Error State
Password must be at least 8 characters
FormGroup(
    Input("password", input_type="password"),
    label="Password",
    error="Password must be at least 8 characters",
    is_invalid=True
)
Success State
Username is available!
FormGroup(
    Input(name="username", value="john_doe"),
    label="Username",
    success="Username is available!",
    is_valid=True
)

2. Required Fields

Automatically add required indicators.

Live Preview
FormGroup(
    Input(name="name"),
    label="Full Name",
    required=True  # Adds red asterisk
)

3. Help Text

Guide users with contextual help.

Live Preview
Find this in your account settings
FormGroup(
    Input(name="api_key"),
    label="API Key",
    help_text="Find this in your account settings"
)

Practical Functionality

Server-Side Validation

Integrate with backend validation.

@app.post("/register")
def register(req):
    email = req.form.get("email")
    password = req.form.get("password")

    # Validate
    errors = {}
    if not email or "@" not in email:
        errors["email"] = "Please enter a valid email"
    if not password or len(password) < 8:
        errors["password"] = "Password must be at least 8 characters"

    if errors:
        # Re-render form with errors
        return Form(
            FormGroup(
                Input(name="email", value=email),
                label="Email",
                error=errors.get("email"),
                is_invalid="email" in errors
            ),
            FormGroup(
                Input("password", input_type="password"),
                label="Password",
                error=errors.get("password"),
                is_invalid="password" in errors
            ),
            Button("Sign Up", type="submit")
        )

    # Success - create user
    create_user(email, password)
    return hx_redirect("/dashboard")

Form Error Summary

Render a compact error alert at the top of the form:

FormErrorSummary(
    errors,
    title="Please fix the following",
    variant="danger",
)

HTMX Live Validation

Validate as users type.

# Form with live validation
FormGroup(
    Input(
        name="username",
        hx_post="/validate/username",
        hx_trigger="keyup changed delay:500ms",
        hx_target="next .feedback"
    ),
    label="Username",
    help_text="3-20 characters, letters and numbers only"
)

# Validation endpoint
@app.post("/validate/username")
def validate_username(username: str):
    if len(username) < 3:
        return Div(
            "Username too short",
            cls="invalid-feedback d-block feedback"
        )
    elif not username.isalnum():
        return Div(
            "Only letters and numbers allowed",
            cls="invalid-feedback d-block feedback"
        )
    else:
        return Div(
            "Username available!",
            cls="valid-feedback d-block feedback"
        )

Complete Registration Form

def RegistrationForm():
    return Form(
        FormGroup(
            Input(name="name"),
            label="Full Name",
            required=True
        ),
        FormGroup(
            Input("email", input_type="email"),
            label="Email Address",
            help_text="We'll never share your email",
            required=True
        ),
        FormGroup(
            Input("password", input_type="password"),
            label="Password",
            help_text="At least 8 characters",
            required=True
        ),
        FormGroup(
            Input("confirm_password", input_type="password"),
            label="Confirm Password",
            required=True
        ),
        Button("Create Account", type="submit", variant="primary", full_width=True),
        hx_post="/register",
        hx_target="#form-container"
    )

Integration Patterns

With Select and Textarea

FormGroup works with any form control.

# Select dropdown
FormGroup(
    Select(
        Option("Select country...", value="", selected=True),
        Option("United States", value="us"),
        Option("United Kingdom", value="uk"),
        name="country"
    ),
    label="Country",
    required=True
)

# Textarea
FormGroup(
    Textarea(name="bio", rows=4),
    label="Bio",
    help_text="Tell us about yourself (optional)"
)

With Custom Input Components

# With SearchableSelect
FormGroup(
    SearchableSelect(
        endpoint="/api/users/search",
        name="assigned_to"
    ),
    label="Assign To",
    help_text="Search by name or email"
)

# With ThemeToggle
FormGroup(
    ThemeToggle(current_theme="dark"),
    label="Appearance"
)

Parameter Reference

Parameter Type Default Description
input_element Any Required Input, Select, or Textarea component
label str \| None None Label text (optional)
help_text str \| None None Help text shown below input
error str \| None None Error message (shown when is_invalid=True)
success str \| None None Success message (shown when is_valid=True)
is_invalid bool False Whether to show invalid state
is_valid bool False Whether to show valid state
required bool False Whether field is required (adds asterisk)
**kwargs Any - Additional HTML attributes for container

Best Practices

✅ Do This

# Use semantic validation
FormGroup(
    Input("email", input_type="email"),
    label="Email",
    error="Please enter a valid email address",
    is_invalid=True
)

# Provide helpful help text
FormGroup(
    Input("password", input_type="password"),
    label="Password",
    help_text="At least 8 characters with 1 number",
    required=True
)

# Show success feedback
FormGroup(
    Input(name="username", value="john_doe"),
    label="Username",
    success="Username is available!",
    is_valid=True
)

❌ Don't Do This

# Don't show both error and success
FormGroup(
    Input(name="test"),
    error="Error!",
    success="Success!",
    is_invalid=True,
    is_valid=True  # Confusing!
)

# Don't use vague error messages
FormGroup(
    Input(name="email"),
    error="Invalid",  # Too vague
    is_invalid=True
)

faststrap.components.forms.formgroup.FormGroup(input_element, label=None, help_text=None, error=None, success=None, is_invalid=False, is_valid=False, required=False, **kwargs)

Form group with label, input, help text, and validation feedback.

Wraps Bootstrap form controls with proper structure and validation states. Eliminates boilerplate for form field creation.

Parameters:

Name Type Description Default
input_element Any

Input, Select, or Textarea component

required
label str | None

Label text (optional)

None
help_text str | None

Help text shown below input

None
error str | None

Error message (shown when is_invalid=True)

None
success str | None

Success message (shown when is_valid=True)

None
is_invalid bool

Whether to show invalid state

False
is_valid bool

Whether to show valid state

False
required bool

Whether field is required (adds asterisk to label)

False
**kwargs Any

Additional HTML attributes for the container

{}

Returns:

Type Description
Div

Div containing label, input, and feedback

Example

Basic form group:

FormGroup( ... Input(name="email", type="email"), ... label="Email Address", ... help_text="We'll never share your email" ... )

With validation error:

FormGroup( ... Input(name="password", type="password"), ... label="Password", ... error="Password must be at least 8 characters", ... is_invalid=True ... )

With success state:

FormGroup( ... Input(name="username", value="john_doe"), ... label="Username", ... success="Username is available!", ... is_valid=True ... )

Required field:

FormGroup( ... Input(name="name"), ... label="Full Name", ... required=True ... )

Note

The input_element should be a FastHTML Input, Select, or Textarea. Validation classes (is-invalid, is-valid) are automatically added to the input element based on is_invalid/is_valid flags.

Source code in src/faststrap/components/forms/formgroup.py
@register(category="forms")
def FormGroup(
    input_element: Any,
    label: str | None = None,
    help_text: str | None = None,
    error: str | None = None,
    success: str | None = None,
    is_invalid: bool = False,
    is_valid: bool = False,
    required: bool = False,
    **kwargs: Any,
) -> Div:
    """Form group with label, input, help text, and validation feedback.

    Wraps Bootstrap form controls with proper structure and validation states.
    Eliminates boilerplate for form field creation.

    Args:
        input_element: Input, Select, or Textarea component
        label: Label text (optional)
        help_text: Help text shown below input
        error: Error message (shown when is_invalid=True)
        success: Success message (shown when is_valid=True)
        is_invalid: Whether to show invalid state
        is_valid: Whether to show valid state
        required: Whether field is required (adds asterisk to label)
        **kwargs: Additional HTML attributes for the container

    Returns:
        Div containing label, input, and feedback

    Example:
        Basic form group:
        >>> FormGroup(
        ...     Input(name="email", type="email"),
        ...     label="Email Address",
        ...     help_text="We'll never share your email"
        ... )

        With validation error:
        >>> FormGroup(
        ...     Input(name="password", type="password"),
        ...     label="Password",
        ...     error="Password must be at least 8 characters",
        ...     is_invalid=True
        ... )

        With success state:
        >>> FormGroup(
        ...     Input(name="username", value="john_doe"),
        ...     label="Username",
        ...     success="Username is available!",
        ...     is_valid=True
        ... )

        Required field:
        >>> FormGroup(
        ...     Input(name="name"),
        ...     label="Full Name",
        ...     required=True
        ... )

    Note:
        The input_element should be a FastHTML Input, Select, or Textarea.
        Validation classes (is-invalid, is-valid) are automatically added
        to the input element based on is_invalid/is_valid flags.
    """
    # Build label
    label_element = None
    if label:
        label_text: Any = label
        if required:
            from fasthtml.common import Span

            label_text = (label, Span(" *", cls="text-danger"))

        label_element = Label(
            label_text,
            cls="form-label",
        )

    # Add validation classes to input
    if hasattr(input_element, "attrs"):
        input_cls = input_element.attrs.get("cls", "")
        if is_invalid:
            input_cls = merge_classes(input_cls, "is-invalid")
        elif is_valid:
            input_cls = merge_classes(input_cls, "is-valid")
        input_element.attrs["cls"] = input_cls

    # Build help text
    help_element = None
    if help_text and not (is_invalid or is_valid):
        help_element = Small(help_text, cls="form-text text-muted")

    # Build validation feedback
    feedback_element = None
    if is_invalid and error:
        feedback_element = Div(error, cls="invalid-feedback d-block")
    elif is_valid and success:
        feedback_element = Div(success, cls="valid-feedback d-block")

    # Build container
    base_classes = ["mb-3"]
    user_cls = kwargs.pop("cls", "")
    all_classes = merge_classes(" ".join(base_classes), user_cls)

    attrs: dict[str, Any] = {"cls": all_classes}
    attrs.update(convert_attrs(kwargs))

    # Assemble form group
    elements = []
    if label_element:
        elements.append(label_element)
    elements.append(input_element)
    if help_element:
        elements.append(help_element)
    if feedback_element:
        elements.append(feedback_element)

    return Div(*elements, **attrs)