Skip to content

Button

The Button component is one of the most fundamental components in web development. In FastStrap, the Button component wraps the standard HTML <button> or <a> element, adding beautiful Bootstrap styling, automatic loading states, and icon support—all in pure Python.

Goal

By the end of this guide, you will be able to create buttons for forms, navigation, and interactive actions, and customize them to fit your exact design needs, even if you've never used Bootstrap before.


Quick Start

Here is the simplest way to create a button.

Live Preview
Button("Click Me", variant="primary")

Visual Examples & Use Cases

1. Variants (Colors = Meaning)

In Bootstrap (and FastStrap), colors carry semantic meaning. You don't just pick "blue"; you pick "Primary" (for the main action).

Live Preview
Button("Primary", variant="primary")
Button("Secondary", variant="secondary")
Button("Success", variant="success")
Button("Danger", variant="danger")
Button("Warning", variant="warning")
Button("Info", variant="info")
Button("Light", variant="light")
Button("Dark", variant="dark")
Button("Link", variant="link")

Understanding Semantic Colors:

Variant Meaning Typical Usage
primary Main Action Submit, Save, Login, Sign Up
secondary Secondary Cancel, Back, More Info
success Positive Confirm, Complete, Uploaded
danger Negative Delete, Remove, Stop
warning Caution Pause, Archive, Reset
info Information Help, About, Status
light Light Backgrounds, App Bars
dark Dark Footer actions, Inverse styling
link Link Look like a text link but act like a button

2. Outline Styles

Sometimes a solid color is too "heavy" for a UI. Use outline=True for a lighter look with a transparent background and colored border.

Live Preview
# A less aggressive 'Delete' button
Button("Delete", variant="danger", outline=True)
Button("Save Draft", variant="primary", outline=True)

3. Sizes

Hierarchy matters. Make your most important buttons larger and secondary actions smaller.

Live Preview
Button("Join Now!", size="lg", variant="primary") # Large Call-to-Action
Button("Default", variant="secondary")            # Default Size
Button("Details", size="sm", variant="info")      # Small table action

4. Full Width Buttons

On mobile devices or in cards, you often want a button to stretch the full width of its container. Use full_width=True.

Live Preview
# A Login Card example
Card(
    Input("email", placeholder="Email"),
    Button("Sign In", variant="primary", full_width=True), # Stretches 100%
    style={"max-width": "300px"}
)

Practical Functionality

Live Preview
# Renders as an <a href="/login" class="btn btn-primary"> tag
Button("Go to Login", as_="a", href="/login", variant="primary")
Live Preview
# Icon at the START (default)
Button("Save", icon="check-circle", variant="success")

# Icon at the END
Button("Next Step", icon="arrow-right", icon_pos="end", variant="primary")
Live Preview
# 1. Shows "Saving..." text
# 2. Shows a spinner
# 3. Disables the button to prevent double-clicks
Button(
    "Save Profile",
    hx_post="/profile/save",
    loading_text="Saving...",
    loading=True # Force state for preview
)

Advanced Customization

1. Override using CSS Variables (The Pro Way)

Bootstrap 5 is built on CSS Variables. This is the best way to customize a specific button (like making a "Purple" brand button) without fighting the framework's CSS rules.

The following variables are available on every button:

CSS Variable Description Example Value
--bs-btn-bg Background color of the button. #6f42c1 (Purple)
--bs-btn-color Text color of the button. #ffffff (White)
--bs-btn-border-color Border color. Usually same as bg. #6f42c1
--bs-btn-hover-bg Background color when hovered. #59359a (Darker Purple)
--bs-btn-hover-color Text color when hovered. #ffffff
--bs-btn-hover-border-color Border color when hovered. #59359a
--bs-btn-border-radius Corner roundness. 2rem (Pill), 0 (Square)
--bs-btn-padding-y Vertical padding (height). 1rem
--bs-btn-padding-x Horizontal padding (width). 2rem
--bs-btn-font-size Text size. 1.25rem

Example: Creating a Custom Purple Button

Live Preview
# Create a dictionary of the variables you want to override
purple_btn_theme = {
    "--bs-btn-bg": "#6f42c1",
    "--bs-btn-border-color": "#6f42c1",
    "--bs-btn-hover-bg": "#59359a",
    "--bs-btn-hover-border-color": "#59359a",
    "--bs-btn-color": "#fff"
}

# Pass it to the css_vars argument
Button("Purple Button", variant="primary", css_vars=purple_btn_theme)

2. Standard CSS Classes

You can pass standard Bootstrap utility classes (or your own classes) using cls.

Live Preview
# 'rounded-pill': Fully rounded corners
# 'shadow-lg': Large drop shadow
# 'text-uppercase': ALL CAPS TEXT
Button("Custom Style", variant="info", cls="rounded-pill shadow-lg text-uppercase")

3. Data Attributes & Accessibility

You can pass any standard HTML attribute. FastStrap cleans them up for you (converting python underscore_names to HTML hyphen-names).

Button(
    "Menu", 
    # Data attributes (Python converts underscores to hyphens)
    data_bs_toggle="dropdown", 
    data_custom_id="123",

    # ARIA attributes for accessibility
    aria_label="Open main menu",
    aria_expanded="false"
)
# Renders: <button data-bs-toggle="dropdown" data-custom-id="123" aria-label="..." ...>

Common "Recipes"

The "Submit & Reset" Toolbar

A common pattern for forms, aligning buttons to the right or left.

Live Preview
def FormActions():
    return Div(
        Button("Submit", type="submit", variant="primary"),
        Button("Reset", type="reset", variant="link", cls="text-decoration-none text-muted"),
        cls="d-flex align-items-center gap-2"
    )

The "Destructive Action"

For actions that can't be undone, use outline-danger to warn the user but not catch the eye too much (to avoid accidental clicks), but switch to solid danger for the confirmation.

Live Preview
Button(
    "Delete Account", 
    variant="outline-danger",
    icon="trash",
    data_bs_toggle="modal", 
    data_bs_target="#confirm-delete-modal"
)

Parameter Reference

This table maps every FastStrap specific parameter to what it actually does in HTML/Bootstrap.

FastStrap Param Type Bootstrap Class / Attribute Description
variant str .btn-{variant} Color theme. Options: primary, secondary, success, danger, warning, info, light, dark, link.
outline bool .btn-outline-{variant} If True, renders outline style instead of solid fill.
size str .btn-{size} Size of button. Options: sm (Small), lg (Large). Default is Medium.
full_width bool .w-100 Makes button span full width of parent.
pill bool .rounded-pill Gives button fully rounded corners.
as_ str <tag> Tag to render. Default button. Use a for links.
href str href="..." URL destination (requires as_="a").
disabled bool disabled / .disabled Disables interactivity and applies disabled styling.
active bool .active Forces the button to appear in a "pressed" state.
icon str <i class="bi bi-{icon}"> Adds a Bootstrap Icon (e.g., "check", "house").
icon_pos str - Position of icon: start (default) or end.
spinner bool .spinner-border Adds a loading spinner.
loading bool - Helper that enables spinner and disabled state together.
loading_text str - Text to display when loading=True.
css_vars dict style="--var: val" Dict of CSS variables to apply inline.

faststrap.components.forms.button.Button(*children, as_='button', variant=None, size=None, outline=False, disabled=False, loading=False, spinner=True, loading_text=None, icon=None, icon_pos='start', icon_cls=None, spinner_pos='start', spinner_cls=None, full_width=False, active=False, pill=False, css_vars=None, style=None, **kwargs)

Bootstrap Button component.

Parameters:

Name Type Description Default
*children Any

Button content (text, elements)

()
as_ Literal['button', 'a']

Render as 'button' or 'a' tag (default: 'button')

'button'
variant VariantType | None

Bootstrap color variant

None
size SizeType | None

Button size (sm, lg)

None
outline bool

Use outline style

False
disabled bool

Disable button

False
loading bool

Show loading spinner and disable

False
spinner bool

Show spinner when loading

True
loading_text str | None

Text to show when loading

None
icon str | None

Bootstrap icon name

None
icon_pos Literal['start', 'end']

Icon position ('start' or 'end')

'start'
icon_cls str | None

Custom classes for icon

None
spinner_pos Literal['start', 'end']

Spinner position ('start' or 'end')

'start'
spinner_cls str | None

Custom classes for spinner

None
full_width bool

Make button 100% width. Note: Can also be achieved via cls='w-100'.

False
active bool

Set active state. Note: Can also be achieved via cls='active'.

False
pill bool

Use rounded-pill style. Note: Can also be achieved via cls='rounded-pill'.

False
css_vars dict[str, Any] | None

Custom CSS variables dictionary

None
style dict[str, Any] | str | None

Custom inline style

None
**kwargs Any

Additional HTML attributes (cls, id, hx-, data-, etc.)

{}
Source code in src/faststrap/components/forms/button.py
@register(category="forms")
def Button(
    *children: Any,
    as_: Literal["button", "a"] = "button",
    variant: VariantType | None = None,
    size: SizeType | None = None,
    outline: bool = False,
    disabled: bool = False,
    loading: bool = False,
    spinner: bool = True,
    loading_text: str | None = None,
    icon: str | None = None,
    icon_pos: Literal["start", "end"] = "start",
    icon_cls: str | None = None,
    spinner_pos: Literal["start", "end"] = "start",
    spinner_cls: str | None = None,
    full_width: bool = False,
    active: bool = False,
    pill: bool = False,
    css_vars: dict[str, Any] | None = None,
    style: dict[str, Any] | str | None = None,
    **kwargs: Any,
) -> FTButton | A:
    """Bootstrap Button component.

    Args:
        *children: Button content (text, elements)
        as_: Render as 'button' or 'a' tag (default: 'button')
        variant: Bootstrap color variant
        size: Button size (sm, lg)
        outline: Use outline style
        disabled: Disable button
        loading: Show loading spinner and disable
        spinner: Show spinner when loading
        loading_text: Text to show when loading
        icon: Bootstrap icon name
        icon_pos: Icon position ('start' or 'end')
        icon_cls: Custom classes for icon
        spinner_pos: Spinner position ('start' or 'end')
        spinner_cls: Custom classes for spinner
        full_width: Make button 100% width. Note: Can also be achieved via `cls='w-100'`.
        active: Set active state. Note: Can also be achieved via `cls='active'`.
        pill: Use rounded-pill style. Note: Can also be achieved via `cls='rounded-pill'`.
        css_vars: Custom CSS variables dictionary
        style: Custom inline style
        **kwargs: Additional HTML attributes (cls, id, hx-*, data-*, etc.)
    """

    # Auto-promote to anchor if href is provided
    if as_ == "button" and "href" in kwargs:
        as_ = "a"

    # Resolve API defaults
    # This automatically picks up global defaults for 'variant', 'size', etc. if the user didn't pass them
    cfg = resolve_defaults(
        "Button",
        variant=variant,
        size=size,
        outline=outline,
        disabled=disabled,
        full_width=full_width,
        pill=pill,
        active=active,
    )

    # Extract resolved values
    c_variant = cfg.get("variant", "primary")  # Hardest fallback
    c_size = cfg.get("size")
    c_outline = cfg.get("outline", False)
    c_full_width = cfg.get("full_width", False)
    c_pill = cfg.get("pill", False)
    c_active = cfg.get("active", False)
    c_disabled = cfg.get("disabled", False)

    # Build base classes
    if c_outline and c_variant != "link":  # "link" doesn't have outline variant
        classes = [f"btn-outline-{c_variant}"]
    else:
        classes = [f"btn-{c_variant}"]

    # Add size class if specified
    if c_size:
        classes.append(f"btn-{c_size}")

    if c_full_width:
        classes.append("w-100")

    if c_pill:
        classes.append("rounded-pill")

    if c_active:
        classes.append("active")

    # Merge with user classes
    user_cls = kwargs.pop("cls", "")
    all_classes = merge_classes("btn", " ".join(classes), user_cls)

    # Build attributes with proper conversion
    attrs: dict[str, Any] = {"cls": all_classes}

    # Optional style + css vars (handled by convert_attrs)
    if style is not None:
        kwargs["style"] = style
    if css_vars is not None:
        kwargs["css_vars"] = css_vars

    # Handle states
    if c_active:
        attrs["aria_pressed"] = "true"

    if loading:
        attrs["aria_busy"] = "true"

    # Disabled/loading behavior differs for <button> vs <a>
    if loading or c_disabled:
        if as_ == "button":
            attrs["disabled"] = True
        else:
            # Bootstrap recommends `.disabled` + aria-disabled for anchors
            attrs["aria_disabled"] = "true"
            attrs["tabindex"] = "-1"
            attrs["cls"] = merge_classes(attrs.get("cls", ""), "disabled")

    # Convert remaining kwargs (including hx_*, data_*, etc.)
    attrs.update(convert_attrs(kwargs))

    # Build content
    content = list(children)

    # Loading: optionally override/augment text
    if loading and loading_text is not None:
        content = [loading_text]

    # Spinner
    if loading and spinner:
        if spinner_pos == "start":
            default_spinner_cls = "spinner-border spinner-border-sm me-2"
        else:
            default_spinner_cls = "spinner-border spinner-border-sm ms-2"

        spinner_elem = Span(
            cls=spinner_cls or default_spinner_cls,
            role="status",
            aria_hidden="true",
        )
        if spinner_pos == "start":
            content.insert(0, spinner_elem)
        else:
            content.append(spinner_elem)

    # Icon
    if (not loading) and icon:
        if icon_pos == "start":
            default_icon_cls = f"bi bi-{icon} me-2"
        else:
            default_icon_cls = f"bi bi-{icon} ms-2"
        icon_elem = I(cls=icon_cls or default_icon_cls, aria_hidden="true")
        if icon_pos == "start":
            content.insert(0, icon_elem)
        else:
            content.append(icon_elem)

    # Render element
    if as_ == "a":
        # Ensure href exists
        if "href" not in attrs:
            attrs["href"] = "#"
        # Keep a11y semantics when used as a button
        attrs.setdefault("role", "button")
        return A(*content, **attrs)

    return FTButton(*content, **attrs)