Skip to content

Components

Components in Starlette-Templates are reusable UI elements built with Jinja2 templates and Pydantic models. They provide:

  • Type safety with Pydantic validation
  • Reusable, testable UI elements
  • Template-based rendering
  • Integration with the Jinja2 environment

Creating Components

Define components using the ComponentModel base class:

from starlette_templates.components.base import ComponentModel
from pydantic import Field

class Alert(ComponentModel):
    # Path to component template
    template: str = "components/alert.html"

    # Component properties with validation
    message: str = Field(..., description="Alert message")
    variant: str = Field(default="info", description="Alert variant")
    dismissible: bool = Field(default=False, description="Show close button")

Component Template

Create the template file (components/alert.html):

<div class="alert alert-{{ variant }}{% if dismissible %} alert-dismissible{% endif %}" role="alert">
    {{ message }}
    {% if dismissible %}
    <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
    {% endif %}
</div>

Using Components

In Routes

Create component instances and pass them to templates:

from starlette.requests import Request
from starlette_templates.responses import TemplateResponse

async def dashboard(request: Request) -> TemplateResponse:
    alert = Alert(
        message="Welcome back!",
        variant="success",
        dismissible=True
    )

    return TemplateResponse(
        "dashboard.html",
        context={"alert": alert}
    )

In Templates

Render components by calling them:

{{ alert }}

Global Registration

Register components globally to use them in any template:

from starlette.applications import Starlette
from starlette.middleware import Middleware
from starlette_templates.middleware import JinjaEnvMiddleware

app = Starlette(
    middleware=[
        Middleware(
            JinjaEnvMiddleware,
            template_loaders=[PackageLoader("myapp", "templates")],
            extra_components=[Alert],  # Register component
        )
    ]
)

Then use directly in templates without passing from routes:

{{ Alert(message="System error", variant="danger", dismissible=True) }}

Built-in Form Components

Starlette Templates includes Bootstrap-compatible form components:

Input

Input - Text, email, password, number inputs:

from starlette_templates.components.forms import Input

# In route
input_field = Input(
    name="username",
    label="Username",
    type="text",
    placeholder="Enter username",
    required=True,
)

# In template
{{ Input(name="email", label="Email", type="email", required=True) }}

Textarea

Textarea - Multi-line text input:

from starlette_templates.components.forms import Textarea

textarea = Textarea(
    name="message",
    label="Message",
    rows=5,
    placeholder="Enter your message",
)

Select

Select - Native select dropdown:

from starlette_templates.components.forms import Select

select = Select(
    name="country",
    label="Country",
    choices={
        "us": "United States",
        "uk": "United Kingdom",
        "ca": "Canada",
    },
    selected="us",
)

Checkbox

Checkbox - Checkbox input:

from starlette_templates.components.forms import Checkbox

checkbox = Checkbox(
    name="agree",
    label="I agree to the terms",
    checked=False,
)

Radio

Radio - Radio button:

from starlette_templates.components.forms import Radio

# In template
{{ Radio(name="plan", label="Basic Plan", value="basic", checked=True) }}
{{ Radio(name="plan", label="Pro Plan", value="pro") }}

Switch

Switch - Toggle switch:

from starlette_templates.components.forms import Switch

switch = Switch(
    name="notifications",
    label="Enable notifications",
    checked=True,
)

FileInput

FileInput - File upload:

from starlette_templates.components.forms import FileInput

file_input = FileInput(
    name="avatar",
    label="Profile Picture",
    accept="image/*",
)

Range

Range - Range slider:

from starlette_templates.components.forms import Range

range_slider = Range(
    name="volume",
    label="Volume",
    min_value=0,
    max_value=100,
    step=1,
    value=50,
)

ChoicesSelect

ChoicesSelect - Enhanced select with Choices.js:

from starlette_templates.components.forms import ChoicesSelect

choices_select = ChoicesSelect(
    name="tags",
    label="Tags",
    choices={
        "python": "Python",
        "javascript": "JavaScript",
        "go": "Go",
    },
    multiple=True,
)

DatePicker

DatePicker - Date picker with Flatpickr:

from starlette_templates.components.forms import DatePicker

date_picker = DatePicker(
    name="birth_date",
    label="Birth Date",
    placeholder="Select date",
)

SubmitButton

SubmitButton - Form submit button:

from starlette_templates.components.forms import SubmitButton

submit_btn = SubmitButton(
    text="Submit Form",
    variant="primary",
)

Component Examples

Card Component

class Card(ComponentModel):
    template: str = "components/card.html"

    title: str = Field(..., description="Card title")
    body: str = Field(..., description="Card body content")
    footer: str | None = Field(None, description="Card footer")
    variant: str = Field(default="default", description="Card style variant")

Template (components/card.html):

<div class="card{% if variant != 'default' %} border-{{ variant }}{% endif %}">
    <div class="card-header">
        <h5 class="card-title">{{ title }}</h5>
    </div>
    <div class="card-body">
        {{ body }}
    </div>
    {% if footer %}
    <div class="card-footer">
        {{ footer }}
    </div>
    {% endif %}
</div>

Usage:

{{ Card(
    title="Welcome",
    body="This is a card component",
    footer="Card footer",
    variant="primary"
) }}

Badge Component

class Badge(ComponentModel):
    template: str = "components/badge.html"

    text: str = Field(..., description="Badge text")
    variant: str = Field(default="primary", description="Badge color")
    pill: bool = Field(default=False, description="Use pill style")

Template (components/badge.html):

<span class="badge bg-{{ variant }}{% if pill %} rounded-pill{% endif %}">
    {{ text }}
</span>

Usage:

{{ Badge(text="New", variant="success", pill=True) }}
{{ Badge(text="Beta", variant="warning") }}

Button Component

class Button(ComponentModel):
    template: str = "components/button.html"

    text: str = Field(..., description="Button text")
    variant: str = Field(default="primary", description="Button style")
    size: str = Field(default="md", description="Button size")
    disabled: bool = Field(default=False, description="Disabled state")
    type: str = Field(default="button", description="Button type")

Template (components/button.html):

<button
    type="{{ type }}"
    class="btn btn-{{ variant }} btn-{{ size }}"
    {% if disabled %}disabled{% endif %}
>
    {{ text }}
</button>

Usage:

{{ Button(text="Click Me", variant="primary") }}
{{ Button(text="Submit", variant="success", type="submit") }}
{{ Button(text="Disabled", disabled=True) }}
class Modal(ComponentModel):
    template: str = "components/modal.html"

    id: str = Field(..., description="Modal ID")
    title: str = Field(..., description="Modal title")
    body: str = Field(..., description="Modal body content")
    footer: str | None = Field(None, description="Modal footer")
    size: str = Field(default="md", description="Modal size (sm, md, lg, xl)")

Template (components/modal.html):

<div class="modal fade" id="{{ id }}" tabindex="-1">
    <div class="modal-dialog modal-{{ size }}">
        <div class="modal-content">
            <div class="modal-header">
                <h5 class="modal-title">{{ title }}</h5>
                <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
            </div>
            <div class="modal-body">
                {{ body }}
            </div>
            {% if footer %}
            <div class="modal-footer">
                {{ footer }}
            </div>
            {% endif %}
        </div>
    </div>
</div>

Component Validation

Components use Pydantic validation to ensure correct usage:

class Image(ComponentModel):
    template: str = "components/image.html"

    src: str = Field(..., description="Image source URL")
    alt: str = Field(..., description="Alt text")
    width: int | None = Field(None, ge=1, description="Image width")
    height: int | None = Field(None, ge=1, description="Image height")

    @field_validator('src')
    @classmethod
    def validate_src(cls, v):
        if not v.startswith(('http://', 'https://', '/')):
            raise ValueError('Invalid image source')
        return v

Nested Components

Components can render other components:

class Alert(ComponentModel):
    template: str = "components/alert.html"
    message: str
    variant: str = "info"

class AlertGroup(ComponentModel):
    template: str = "components/alert_group.html"
    alerts: list[Alert] = Field(default_factory=list)

Template (components/alert_group.html):

<div class="alert-group">
    {% for alert in alerts %}
    {{ alert }}
    {% endfor %}
</div>

Usage:

alert_group = AlertGroup(
    alerts=[
        Alert(message="Success!", variant="success"),
        Alert(message="Warning!", variant="warning"),
        Alert(message="Error!", variant="danger"),
    ]
)