Skip to content

tempestweb.components

Ready-made Material 3 fields, forms and buttons, including the Brazilian ones (CPF, CNPJ, phone, address) and the validators that go with them. These are content components; the screen's frame comes from tempestweb.presets or is assembled by you.

Guide with examples: Ready-made components.

tempestweb.components

tempestweb.components — ready-to-use fields, forms and Material 3 buttons.

Pre-built, validated building blocks so an app declares a field, a button or a whole form in one line instead of wiring inputs, labels, errors and validators by hand:

from tempestweb.components import EmailField, PasswordField, LoginForm
from tempestweb.components import filled_button, text_button

EmailField/PasswordField and TextField are tempestweb-native, styled Material 3 against the theme they are given; the BR fields (:class:PhoneField/:class:CPFField/:class:CNPJField/:class:AddressField) wrap the core's masked inputs. The *_button helpers build the MD3 button variants. Everything renders identically in Mode A (WASM) and Mode B (server).

Pass theme=app.theme to a field or a form exactly as you would to a widget: the theme reaches the Input each field builds, the fields and submit button each form builds, and the colour of every label and error line.

EmailField

Bases: Component

A labelled, Material 3 e-mail field that follows the theme it is given.

The tempestweb-native e-mail field: a muted label, a controlled :class:~tempest_core.widgets.inputs.Input on the e-mail keyboard, and an optional error line. tempest-core resolves the Input's outlined Material 3 style inline against theme, so it matches the rest of a tempestweb UI in light and in dark alike.

Validate with :func:validate_email.

Attributes:

Name Type Description
value str

The current e-mail value (controlled).

label str

The label shown above the field (omitted when empty).

placeholder str

The empty-field hint.

error str

The validation message; shown in the theme's error colour.

on_change Callable[[str], Any]

Called with the new string value on each edit.

theme Theme

The theme the input, label and error line resolve against.

Source code in tempestweb/components/fields.py
class EmailField(Component):
    """A labelled, Material 3 e-mail field that follows the theme it is given.

    The tempestweb-native e-mail field: a muted label, a controlled
    :class:`~tempest_core.widgets.inputs.Input` on the e-mail keyboard, and an
    optional error line. tempest-core resolves the Input's outlined Material 3
    style inline **against ``theme``**, so it matches the rest of a tempestweb UI
    in light and in dark alike.

    Validate with :func:`validate_email`.

    Attributes:
        value: The current e-mail value (controlled).
        label: The label shown above the field (omitted when empty).
        placeholder: The empty-field hint.
        error: The validation message; shown in the theme's error colour.
        on_change: Called with the new string value on each edit.
        theme: The theme the input, label and error line resolve against.
    """

    value: str = Field(default="", description="The current e-mail value (controlled).")
    label: str = Field(default="E-mail", description="The label shown above the field.")
    placeholder: str = Field(
        default="you@example.com", description="The empty-field hint."
    )
    error: str = Field(default="", description="Validation message (shown when set).")
    on_change: Callable[[str], Any] = Field(
        description="Called with the new string value on each edit."
    )
    theme: Theme = Field(
        default_factory=current_theme,
        description="The theme the field's input, label and error resolve against.",
    )

    def render(self) -> Widget:
        """Lower the e-mail field into a labelled column wrapping an Input.

        Returns:
            A :class:`~tempest_core.Column` with the optional label, the e-mail
            input, and the optional error line.
        """
        on_change = self.on_change

        def _emit(event: TextChangeEvent) -> None:
            """Unwrap the core's change event and hand the plain string over.

            The field's public ``on_change`` takes a ``str``, not an event, so
            the component absorbs the widget-level event shape. It closes over
            the callable captured above rather than over ``self``, so the
            handler does not keep the component instance alive.

            Args:
                event: The input's change event, carrying the new value.
            """
            on_change(event.value)

        base = self.key or "email-field"
        control_name = _control_semantics(self.label, self.semantics)
        field = Input(
            value=self.value,
            placeholder=self.placeholder,
            keyboard=KeyboardType.EMAIL,
            on_change=_emit,
            theme=self.theme,
            semantics=control_name,
            key=f"{base}-input",
        )
        return _labelled_field(self.label, field, self.error, base, self.theme)

render

render() -> Widget

Lower the e-mail field into a labelled column wrapping an Input.

Returns:

Name Type Description
A Widget

class:~tempest_core.Column with the optional label, the e-mail

Widget

input, and the optional error line.

Source code in tempestweb/components/fields.py
def render(self) -> Widget:
    """Lower the e-mail field into a labelled column wrapping an Input.

    Returns:
        A :class:`~tempest_core.Column` with the optional label, the e-mail
        input, and the optional error line.
    """
    on_change = self.on_change

    def _emit(event: TextChangeEvent) -> None:
        """Unwrap the core's change event and hand the plain string over.

        The field's public ``on_change`` takes a ``str``, not an event, so
        the component absorbs the widget-level event shape. It closes over
        the callable captured above rather than over ``self``, so the
        handler does not keep the component instance alive.

        Args:
            event: The input's change event, carrying the new value.
        """
        on_change(event.value)

    base = self.key or "email-field"
    control_name = _control_semantics(self.label, self.semantics)
    field = Input(
        value=self.value,
        placeholder=self.placeholder,
        keyboard=KeyboardType.EMAIL,
        on_change=_emit,
        theme=self.theme,
        semantics=control_name,
        key=f"{base}-input",
    )
    return _labelled_field(self.label, field, self.error, base, self.theme)

PasswordField

Bases: Component

A labelled, secure password field that follows the theme it is given.

Like :class:EmailField but the input is secure (masked). It carries no inline style of its own: the core resolves the outlined Material 3 treatment from theme, so the field is light in a light app and dark in a dark one.

Attributes:

Name Type Description
value str

The current password value (controlled).

label str

The label shown above the field (omitted when empty).

placeholder str

The empty-field hint.

error str

The validation message; shown in the theme's error colour.

on_change Callable[[str], Any]

Called with the new string value on each edit.

theme Theme

The theme the input, label and error line resolve against.

Source code in tempestweb/components/fields.py
class PasswordField(Component):
    """A labelled, secure password field that follows the theme it is given.

    Like :class:`EmailField` but the input is ``secure`` (masked). It carries no
    inline style of its own: the core resolves the outlined Material 3 treatment
    from ``theme``, so the field is light in a light app and dark in a dark one.

    Attributes:
        value: The current password value (controlled).
        label: The label shown above the field (omitted when empty).
        placeholder: The empty-field hint.
        error: The validation message; shown in the theme's error colour.
        on_change: Called with the new string value on each edit.
        theme: The theme the input, label and error line resolve against.
    """

    value: str = Field(default="", description="The current password (controlled).")
    label: str = Field(default="Senha", description="The label shown above the field.")
    placeholder: str = Field(default="", description="The empty-field hint.")
    error: str = Field(default="", description="Validation message (shown when set).")
    on_change: Callable[[str], Any] = Field(
        description="Called with the new string value on each edit."
    )
    theme: Theme = Field(
        default_factory=current_theme,
        description="The theme the field's input, label and error resolve against.",
    )

    def render(self) -> Widget:
        """Lower the password field into a labelled column wrapping an Input.

        Returns:
            A :class:`~tempest_core.Column` with the optional label, the secure
            input, and the optional error line.
        """
        on_change = self.on_change

        def _emit(event: TextChangeEvent) -> None:
            """Unwrap the core's change event and hand the plain string over.

            The field's public ``on_change`` takes a ``str``, not an event, so
            the component absorbs the widget-level event shape. It closes over
            the callable captured above rather than over ``self``, so the
            handler does not keep the component instance alive.

            Args:
                event: The input's change event, carrying the new value.
            """
            on_change(event.value)

        base = self.key or "password-field"
        control_name = _control_semantics(self.label, self.semantics)
        field = Input(
            value=self.value,
            placeholder=self.placeholder,
            secure=True,
            on_change=_emit,
            theme=self.theme,
            semantics=control_name,
            key=f"{base}-input",
        )
        return _labelled_field(self.label, field, self.error, base, self.theme)

render

render() -> Widget

Lower the password field into a labelled column wrapping an Input.

Returns:

Name Type Description
A Widget

class:~tempest_core.Column with the optional label, the secure

Widget

input, and the optional error line.

Source code in tempestweb/components/fields.py
def render(self) -> Widget:
    """Lower the password field into a labelled column wrapping an Input.

    Returns:
        A :class:`~tempest_core.Column` with the optional label, the secure
        input, and the optional error line.
    """
    on_change = self.on_change

    def _emit(event: TextChangeEvent) -> None:
        """Unwrap the core's change event and hand the plain string over.

        The field's public ``on_change`` takes a ``str``, not an event, so
        the component absorbs the widget-level event shape. It closes over
        the callable captured above rather than over ``self``, so the
        handler does not keep the component instance alive.

        Args:
            event: The input's change event, carrying the new value.
        """
        on_change(event.value)

    base = self.key or "password-field"
    control_name = _control_semantics(self.label, self.semantics)
    field = Input(
        value=self.value,
        placeholder=self.placeholder,
        secure=True,
        on_change=_emit,
        theme=self.theme,
        semantics=control_name,
        key=f"{base}-input",
    )
    return _labelled_field(self.label, field, self.error, base, self.theme)

TextField

Bases: Component

A generic labelled text field for arbitrary input (name, title, …).

The general-purpose sibling of the BR-specific fields: a label, a controlled text :class:~tempest_core.Input, and an optional error message. Controlled — pass value and an on_change that stores the new string.

Attributes:

Name Type Description
value str

The current text value (controlled).

label str

The label shown above the field (omitted when empty).

placeholder str

The empty-field hint.

error str

The validation message; shown in the theme's error colour.

on_change Callable[[str], Any]

Called with the new string value on each edit.

theme Theme

The theme the input, label and error line resolve against.

Source code in tempestweb/components/fields.py
class TextField(Component):
    """A generic labelled text field for arbitrary input (name, title, …).

    The general-purpose sibling of the BR-specific fields: a label, a controlled
    text :class:`~tempest_core.Input`, and an optional error message. Controlled —
    pass ``value`` and an ``on_change`` that stores the new string.

    Attributes:
        value: The current text value (controlled).
        label: The label shown above the field (omitted when empty).
        placeholder: The empty-field hint.
        error: The validation message; shown in the theme's error colour.
        on_change: Called with the new string value on each edit.
        theme: The theme the input, label and error line resolve against.
    """

    value: str = Field(default="", description="The current text value (controlled).")
    label: str = Field(default="", description="The label shown above the field.")
    placeholder: str = Field(default="", description="The empty-field hint.")
    error: str = Field(default="", description="Validation message (shown when set).")
    on_change: Callable[[str], Any] = Field(
        description="Called with the new string value on each edit."
    )
    theme: Theme = Field(
        default_factory=current_theme,
        description="The theme the field's input, label and error resolve against.",
    )

    def render(self) -> Widget:
        """Lower the field into a labelled column wrapping a text Input.

        The inner widgets' keys are derived from this component's ``key``. They
        used to be literals, so two fields on the same screen shared the key of
        the ``Input`` that actually emits the events and the router could not
        tell them apart — an edit applied to the wrong field, silently.

        Returns:
            A :class:`~tempest_core.Column` with the optional label, the text
            input, and the optional error line.
        """
        on_change = self.on_change

        def _emit(event: TextChangeEvent) -> None:
            """Unwrap the core's change event and hand the plain string over.

            The field's public ``on_change`` takes a ``str``, not an event, so
            the component absorbs the widget-level event shape. It closes over
            the callable captured above rather than over ``self``, so the
            handler does not keep the component instance alive.

            Args:
                event: The input's change event, carrying the new value.
            """
            on_change(event.value)

        base = self.key or "text-field"
        control_name = _control_semantics(self.label, self.semantics)
        children: list[Widget] = []
        if self.label:
            children.append(Text(content=self.label, key=f"{base}-label"))
        children.append(
            Input(
                value=self.value,
                placeholder=self.placeholder,
                on_change=_emit,
                theme=self.theme,
                semantics=control_name,
                key=f"{base}-input",
            )
        )
        if self.error:
            children.append(
                Text(
                    content=self.error,
                    key=f"{base}-error",
                    style=Style(color=_error_color(self.theme)),
                )
            )
        return Column(
            key=base,
            style=Style(gap=4.0, padding=Edge.symmetric(vertical=4.0)),
            children=children,
        )

render

render() -> Widget

Lower the field into a labelled column wrapping a text Input.

The inner widgets' keys are derived from this component's key. They used to be literals, so two fields on the same screen shared the key of the Input that actually emits the events and the router could not tell them apart — an edit applied to the wrong field, silently.

Returns:

Name Type Description
A Widget

class:~tempest_core.Column with the optional label, the text

Widget

input, and the optional error line.

Source code in tempestweb/components/fields.py
def render(self) -> Widget:
    """Lower the field into a labelled column wrapping a text Input.

    The inner widgets' keys are derived from this component's ``key``. They
    used to be literals, so two fields on the same screen shared the key of
    the ``Input`` that actually emits the events and the router could not
    tell them apart — an edit applied to the wrong field, silently.

    Returns:
        A :class:`~tempest_core.Column` with the optional label, the text
        input, and the optional error line.
    """
    on_change = self.on_change

    def _emit(event: TextChangeEvent) -> None:
        """Unwrap the core's change event and hand the plain string over.

        The field's public ``on_change`` takes a ``str``, not an event, so
        the component absorbs the widget-level event shape. It closes over
        the callable captured above rather than over ``self``, so the
        handler does not keep the component instance alive.

        Args:
            event: The input's change event, carrying the new value.
        """
        on_change(event.value)

    base = self.key or "text-field"
    control_name = _control_semantics(self.label, self.semantics)
    children: list[Widget] = []
    if self.label:
        children.append(Text(content=self.label, key=f"{base}-label"))
    children.append(
        Input(
            value=self.value,
            placeholder=self.placeholder,
            on_change=_emit,
            theme=self.theme,
            semantics=control_name,
            key=f"{base}-input",
        )
    )
    if self.error:
        children.append(
            Text(
                content=self.error,
                key=f"{base}-error",
                style=Style(color=_error_color(self.theme)),
            )
        )
    return Column(
        key=base,
        style=Style(gap=4.0, padding=Edge.symmetric(vertical=4.0)),
        children=children,
    )

LoginForm

Bases: Component

A complete email + password login form with a submit button.

The app keeps email/password in its state and updates them via the on_*_change handlers; on_submit fires when the button is pressed (the app reads the current values from its state and performs the login). Pass email_error/password_error to surface validation messages.

Example
def set_email(v: str) -> None:
    app.set_state(lambda s: setattr(s, "email", v))

LoginForm(
    email=app.state.email,
    password=app.state.password,
    on_email_change=set_email,
    on_password_change=set_password,
    on_submit=do_login,
    email_error=app.state.email_error,
)

Attributes:

Name Type Description
email str

The current e-mail value (controlled).

password str

The current password value (controlled).

on_email_change Callable[[str], Any]

Called with the new e-mail string on each edit.

on_password_change Callable[[str], Any]

Called with the new password string on each edit.

on_submit Callable[[], Any]

Called when the submit button is pressed.

email_error str

Validation message under the e-mail field (shown when set).

password_error str

Validation message under the password field.

title str

Optional heading shown above the fields.

submit_label str

The submit button label.

theme Theme

The theme handed to every field and to the submit button.

Source code in tempestweb/components/forms.py
class LoginForm(Component):
    """A complete email + password login form with a submit button.

    The app keeps ``email``/``password`` in its state and updates them via the
    ``on_*_change`` handlers; ``on_submit`` fires when the button is pressed (the
    app reads the current values from its state and performs the login). Pass
    ``email_error``/``password_error`` to surface validation messages.

    Example:
        ```python
        def set_email(v: str) -> None:
            app.set_state(lambda s: setattr(s, "email", v))

        LoginForm(
            email=app.state.email,
            password=app.state.password,
            on_email_change=set_email,
            on_password_change=set_password,
            on_submit=do_login,
            email_error=app.state.email_error,
        )
        ```

    Attributes:
        email: The current e-mail value (controlled).
        password: The current password value (controlled).
        on_email_change: Called with the new e-mail string on each edit.
        on_password_change: Called with the new password string on each edit.
        on_submit: Called when the submit button is pressed.
        email_error: Validation message under the e-mail field (shown when set).
        password_error: Validation message under the password field.
        title: Optional heading shown above the fields.
        submit_label: The submit button label.
        theme: The theme handed to every field and to the submit button.
    """

    email: str = Field(default="", description="The current e-mail value.")
    password: str = Field(default="", description="The current password value.")
    on_email_change: Callable[[str], Any] = Field(
        description="Called with the new e-mail string on each edit."
    )
    on_password_change: Callable[[str], Any] = Field(
        description="Called with the new password string on each edit."
    )
    on_submit: Callable[[], Any] = Field(
        description="Called when the submit button is pressed."
    )
    email_error: str = Field(default="", description="E-mail validation message.")
    password_error: str = Field(default="", description="Password validation message.")
    title: str = Field(default="", description="Optional heading above the fields.")
    submit_label: str = Field(default="Entrar", description="Submit button label.")
    theme: Theme = Field(
        default_factory=current_theme,
        description="The theme the fields and the submit button resolve against.",
    )

    def render(self) -> Widget:
        """Lay out the title, fields and submit button in a column.

        Returns:
            A :class:`~tempest_core.Column` containing the optional title, the
            e-mail and password fields, and the submit button.
        """
        base = self.key or "login"
        children: list[Widget] = []
        if self.title:
            children.append(Text(content=self.title, key=f"{base}-title"))
        children.extend(
            [
                EmailField(
                    value=self.email,
                    on_change=self.on_email_change,
                    error=self.email_error,
                    theme=self.theme,
                    key=f"{base}-email",
                ),
                PasswordField(
                    value=self.password,
                    on_change=self.on_password_change,
                    error=self.password_error,
                    theme=self.theme,
                    key=f"{base}-password",
                ),
                Button(
                    label=self.submit_label,
                    on_click=self.on_submit,
                    theme=self.theme,
                    key=f"{base}-submit",
                ),
            ]
        )
        return Column(
            key=self.key or "login-form",
            style=Style(gap=12.0, padding=Edge.all(16)),
            children=children,
            semantics=self.semantics,
        )

render

render() -> Widget

Lay out the title, fields and submit button in a column.

Returns:

Name Type Description
A Widget

class:~tempest_core.Column containing the optional title, the

Widget

e-mail and password fields, and the submit button.

Source code in tempestweb/components/forms.py
def render(self) -> Widget:
    """Lay out the title, fields and submit button in a column.

    Returns:
        A :class:`~tempest_core.Column` containing the optional title, the
        e-mail and password fields, and the submit button.
    """
    base = self.key or "login"
    children: list[Widget] = []
    if self.title:
        children.append(Text(content=self.title, key=f"{base}-title"))
    children.extend(
        [
            EmailField(
                value=self.email,
                on_change=self.on_email_change,
                error=self.email_error,
                theme=self.theme,
                key=f"{base}-email",
            ),
            PasswordField(
                value=self.password,
                on_change=self.on_password_change,
                error=self.password_error,
                theme=self.theme,
                key=f"{base}-password",
            ),
            Button(
                label=self.submit_label,
                on_click=self.on_submit,
                theme=self.theme,
                key=f"{base}-submit",
            ),
        ]
    )
    return Column(
        key=self.key or "login-form",
        style=Style(gap=12.0, padding=Edge.all(16)),
        children=children,
        semantics=self.semantics,
    )

SignupForm

Bases: Component

An email + password + confirm-password sign-up form with a submit button.

Like :class:LoginForm it is controlled: the app holds the three values in state and updates them via the on_*_change handlers. on_submit fires when the button is pressed; surface validation via the *_error fields (e.g. set confirm_error when the passwords differ).

Attributes:

Name Type Description
email str

The current e-mail value (controlled).

password str

The current password value (controlled).

confirm str

The current confirm-password value (controlled).

on_email_change Callable[[str], Any]

Called with the new e-mail string on each edit.

on_password_change Callable[[str], Any]

Called with the new password string on each edit.

on_confirm_change Callable[[str], Any]

Called with the new confirm-password string on each edit.

on_submit Callable[[], Any]

Called when the submit button is pressed.

email_error str

Validation message under the e-mail field.

password_error str

Validation message under the password field.

confirm_error str

Validation message under the confirm-password field.

title str

Optional heading shown above the fields.

submit_label str

The submit button label.

theme Theme

The theme handed to every field and to the submit button.

Source code in tempestweb/components/forms.py
class SignupForm(Component):
    """An email + password + confirm-password sign-up form with a submit button.

    Like :class:`LoginForm` it is controlled: the app holds the three values in
    state and updates them via the ``on_*_change`` handlers. ``on_submit`` fires
    when the button is pressed; surface validation via the ``*_error`` fields
    (e.g. set ``confirm_error`` when the passwords differ).

    Attributes:
        email: The current e-mail value (controlled).
        password: The current password value (controlled).
        confirm: The current confirm-password value (controlled).
        on_email_change: Called with the new e-mail string on each edit.
        on_password_change: Called with the new password string on each edit.
        on_confirm_change: Called with the new confirm-password string on each edit.
        on_submit: Called when the submit button is pressed.
        email_error: Validation message under the e-mail field.
        password_error: Validation message under the password field.
        confirm_error: Validation message under the confirm-password field.
        title: Optional heading shown above the fields.
        submit_label: The submit button label.
        theme: The theme handed to every field and to the submit button.
    """

    email: str = Field(default="", description="The current e-mail value.")
    password: str = Field(default="", description="The current password value.")
    confirm: str = Field(default="", description="The current confirm value.")
    on_email_change: Callable[[str], Any] = Field(
        description="Called with the new e-mail string on each edit."
    )
    on_password_change: Callable[[str], Any] = Field(
        description="Called with the new password string on each edit."
    )
    on_confirm_change: Callable[[str], Any] = Field(
        description="Called with the new confirm-password string on each edit."
    )
    on_submit: Callable[[], Any] = Field(
        description="Called when the submit button is pressed."
    )
    email_error: str = Field(default="", description="E-mail validation message.")
    password_error: str = Field(default="", description="Password validation message.")
    confirm_error: str = Field(default="", description="Confirm validation message.")
    title: str = Field(default="", description="Optional heading above the fields.")
    submit_label: str = Field(default="Cadastrar", description="Submit button label.")
    theme: Theme = Field(
        default_factory=current_theme,
        description="The theme the fields and the submit button resolve against.",
    )

    def render(self) -> Widget:
        """Lay out the title, fields and submit button in a column.

        Returns:
            A :class:`~tempest_core.Column` with the optional title, the e-mail,
            password and confirm-password fields, and the submit button.
        """
        base = self.key or "signup"
        children: list[Widget] = []
        if self.title:
            children.append(Text(content=self.title, key=f"{base}-title"))
        children.extend(
            [
                EmailField(
                    value=self.email,
                    on_change=self.on_email_change,
                    error=self.email_error,
                    theme=self.theme,
                    key=f"{base}-email",
                ),
                PasswordField(
                    value=self.password,
                    on_change=self.on_password_change,
                    error=self.password_error,
                    theme=self.theme,
                    key=f"{base}-password",
                ),
                PasswordField(
                    value=self.confirm,
                    on_change=self.on_confirm_change,
                    error=self.confirm_error,
                    label="Confirmar senha",
                    theme=self.theme,
                    key=f"{base}-confirm",
                ),
                Button(
                    label=self.submit_label,
                    on_click=self.on_submit,
                    theme=self.theme,
                    key=f"{base}-submit",
                ),
            ]
        )
        return Column(
            key=self.key or "signup-form",
            style=Style(gap=12.0, padding=Edge.all(16)),
            children=children,
            semantics=self.semantics,
        )

render

render() -> Widget

Lay out the title, fields and submit button in a column.

Returns:

Name Type Description
A Widget

class:~tempest_core.Column with the optional title, the e-mail,

Widget

password and confirm-password fields, and the submit button.

Source code in tempestweb/components/forms.py
def render(self) -> Widget:
    """Lay out the title, fields and submit button in a column.

    Returns:
        A :class:`~tempest_core.Column` with the optional title, the e-mail,
        password and confirm-password fields, and the submit button.
    """
    base = self.key or "signup"
    children: list[Widget] = []
    if self.title:
        children.append(Text(content=self.title, key=f"{base}-title"))
    children.extend(
        [
            EmailField(
                value=self.email,
                on_change=self.on_email_change,
                error=self.email_error,
                theme=self.theme,
                key=f"{base}-email",
            ),
            PasswordField(
                value=self.password,
                on_change=self.on_password_change,
                error=self.password_error,
                theme=self.theme,
                key=f"{base}-password",
            ),
            PasswordField(
                value=self.confirm,
                on_change=self.on_confirm_change,
                error=self.confirm_error,
                label="Confirmar senha",
                theme=self.theme,
                key=f"{base}-confirm",
            ),
            Button(
                label=self.submit_label,
                on_click=self.on_submit,
                theme=self.theme,
                key=f"{base}-submit",
            ),
        ]
    )
    return Column(
        key=self.key or "signup-form",
        style=Style(gap=12.0, padding=Edge.all(16)),
        children=children,
        semantics=self.semantics,
    )

elevated_button

elevated_button(label: str, on_click: Callable[[], Any] | None = None, *, key: str | None = None) -> Button

Build a Material 3 elevated button — a filled button with a resting shadow.

The core has no elevated variant, so this layers a small inline :class:~tempest_core.style.Shadow over the SOLID / primary fill; the core merges the shadow override onto the variant-resolved base style.

Parameters:

Name Type Description Default
label str

The button text.

required
on_click Callable[[], Any] | None

Handler fired when the button is pressed.

None
key str | None

Optional reconciler key.

None

Returns:

Name Type Description
A Button

class:~tempest_core.Button rendered as an MD3 elevated button.

Source code in tempestweb/components/buttons.py
def elevated_button(
    label: str,
    on_click: Callable[[], Any] | None = None,
    *,
    key: str | None = None,
) -> Button:
    """Build a Material 3 *elevated* button — a filled button with a resting shadow.

    The core has no elevated variant, so this layers a small inline
    :class:`~tempest_core.style.Shadow` over the ``SOLID`` / ``primary`` fill; the
    core merges the shadow override onto the variant-resolved base style.

    Args:
        label: The button text.
        on_click: Handler fired when the button is pressed.
        key: Optional reconciler key.

    Returns:
        A :class:`~tempest_core.Button` rendered as an MD3 elevated button.
    """
    return Button(
        label=label,
        on_click=on_click,
        key=key,
        variant=Variant.SOLID,
        color_scheme="primary",
        style=Style(
            shadow=Shadow(
                color=Color(r=0, g=0, b=0, a=0.3), blur=3.0, offset_x=0.0, offset_y=1.0
            ),
        ),
    )

filled_button

filled_button(label: str, on_click: Callable[[], Any] | None = None, *, key: str | None = None) -> Button

Build a Material 3 filled button — the high-emphasis default.

Delegates to the core SOLID variant on the primary color scheme, so the primary fill and white label come from the core's resolved style.

Parameters:

Name Type Description Default
label str

The button text.

required
on_click Callable[[], Any] | None

Handler fired when the button is pressed.

None
key str | None

Optional reconciler key.

None

Returns:

Name Type Description
A Button

class:~tempest_core.Button rendered as an MD3 filled button.

Source code in tempestweb/components/buttons.py
def filled_button(
    label: str,
    on_click: Callable[[], Any] | None = None,
    *,
    key: str | None = None,
) -> Button:
    """Build a Material 3 *filled* button — the high-emphasis default.

    Delegates to the core ``SOLID`` variant on the ``primary`` color scheme, so
    the primary fill and white label come from the core's resolved style.

    Args:
        label: The button text.
        on_click: Handler fired when the button is pressed.
        key: Optional reconciler key.

    Returns:
        A :class:`~tempest_core.Button` rendered as an MD3 filled button.
    """
    return Button(
        label=label,
        on_click=on_click,
        key=key,
        variant=Variant.SOLID,
        color_scheme="primary",
    )

outlined_button

outlined_button(label: str, on_click: Callable[[], Any] | None = None, *, key: str | None = None) -> Button

Build a Material 3 outlined button — medium emphasis with a border.

Delegates to the core OUTLINE variant, whose resolved style draws the primary-toned border.

Parameters:

Name Type Description Default
label str

The button text.

required
on_click Callable[[], Any] | None

Handler fired when the button is pressed.

None
key str | None

Optional reconciler key.

None

Returns:

Name Type Description
A Button

class:~tempest_core.Button rendered as an MD3 outlined button.

Source code in tempestweb/components/buttons.py
def outlined_button(
    label: str,
    on_click: Callable[[], Any] | None = None,
    *,
    key: str | None = None,
) -> Button:
    """Build a Material 3 *outlined* button — medium emphasis with a border.

    Delegates to the core ``OUTLINE`` variant, whose resolved style draws the
    primary-toned border.

    Args:
        label: The button text.
        on_click: Handler fired when the button is pressed.
        key: Optional reconciler key.

    Returns:
        A :class:`~tempest_core.Button` rendered as an MD3 outlined button.
    """
    return Button(
        label=label,
        on_click=on_click,
        key=key,
        variant=Variant.OUTLINE,
    )

text_button

text_button(label: str, on_click: Callable[[], Any] | None = None, *, key: str | None = None) -> Button

Build a Material 3 text button — low emphasis, no border.

Delegates to the core GHOST variant, the lowest-emphasis button: a bare label with no border, the base sheet's state layer still tinting it on hover/press.

Parameters:

Name Type Description Default
label str

The button text.

required
on_click Callable[[], Any] | None

Handler fired when the button is pressed.

None
key str | None

Optional reconciler key.

None

Returns:

Name Type Description
A Button

class:~tempest_core.Button rendered as an MD3 text button.

Source code in tempestweb/components/buttons.py
def text_button(
    label: str,
    on_click: Callable[[], Any] | None = None,
    *,
    key: str | None = None,
) -> Button:
    """Build a Material 3 *text* button — low emphasis, no border.

    Delegates to the core ``GHOST`` variant, the lowest-emphasis button: a bare
    label with no border, the base sheet's state layer still tinting it on
    hover/press.

    Args:
        label: The button text.
        on_click: Handler fired when the button is pressed.
        key: Optional reconciler key.

    Returns:
        A :class:`~tempest_core.Button` rendered as an MD3 text button.
    """
    return Button(
        label=label,
        on_click=on_click,
        key=key,
        variant=Variant.GHOST,
    )

tonal_button

tonal_button(label: str, on_click: Callable[[], Any] | None = None, *, key: str | None = None) -> Button

Build a Material 3 filled tonal button — medium emphasis.

Delegates to the core SOLID variant on the secondary color scheme for a lower-emphasis, secondary-toned fill.

Parameters:

Name Type Description Default
label str

The button text.

required
on_click Callable[[], Any] | None

Handler fired when the button is pressed.

None
key str | None

Optional reconciler key.

None

Returns:

Name Type Description
A Button

class:~tempest_core.Button rendered as an MD3 filled tonal button.

Source code in tempestweb/components/buttons.py
def tonal_button(
    label: str,
    on_click: Callable[[], Any] | None = None,
    *,
    key: str | None = None,
) -> Button:
    """Build a Material 3 *filled tonal* button — medium emphasis.

    Delegates to the core ``SOLID`` variant on the ``secondary`` color scheme for
    a lower-emphasis, secondary-toned fill.

    Args:
        label: The button text.
        on_click: Handler fired when the button is pressed.
        key: Optional reconciler key.

    Returns:
        A :class:`~tempest_core.Button` rendered as an MD3 filled tonal button.
    """
    return Button(
        label=label,
        on_click=on_click,
        key=key,
        variant=Variant.SOLID,
        color_scheme="secondary",
    )