PyBladePyBlade

Components

As your application grows, you will often have UI elements that appear in many places: buttons, cards, alerts, navigation items, and more.

You could create these elements as partials and include them wherever needed. However, some UI elements also need their own data, configuration, or behavior. This is where components become useful.

Instead of writing the same HTML again and again, PyBlade components let you define a reusable piece of UI once and use it throughout your application with a simple, consistent syntax.

Let's see how to create components, pass data to them, and customize their content.

Creating a component

A component is simply a .html file that can be created manually, but for convenience, PyBlade provides the pyblade make:component command to generate a new component automatically.

By default, components must be stored inside the components folder, located within your project root directory:

alert.html
card.html
...
pyblade.toml

You can manually create a component inside the components folder, or use the provided command to generate one:

pyblade make:component alert

This command will create the file: components/alert.html

alert.html

It is important to avoid naming your component slot, as this could lead to conflicts with PyBlade's built-in pb-slot tag used for creating new slots. Rendering a component named slot will interfere with the slot system, causing potential issues.

Rendering components

The preferred way to include a component in your template is by using a PyBlade component tag. Component tags start with pb-, followed by the component file name without extension.

For example, if you have a component file named alert.html, you can include it in your template like this:

<pb-alert />

Similarly, for a user-profile.html or user_profile.html component, you would write:

<pb-user-profile />

A PyBlade component tag can be self-closing — when the component does not need any inner content:

<pb-alert />

or paired — when you need to include content inside the component:

<pb-alert>
    This is an important message!
</pb-alert>

The @component directive

PyBlade also provides an alternative way to render components using the @component directive:

@component("alert")
@component("user-profile")

While this method works, it is not as intuitive as using component tags. The tag-based syntax is visually clearer and aligns with HTML syntax.

Handling nested components

If your components are stored inside subdirectories within components/, you can indicate this hierarchy using a dot notation in both approaches.

For instance, if you have a component file located at components/forms/dropdown.html, you can render it as follows:

<pb-forms.dropdown />

or, alternatively:

@component('forms.dropdown')

Passing data to components

When creating a component, it often expects certain values (variables) to be passed in when used. You can pass data to PyBlade components using HTML attributes. Hard-coded, primitive values may be passed to the component using simple HTML attribute strings. Python expressions and variables should be passed to the component via attributes that use the : character as a prefix.

If you're using the @component directive to render a component, you may pass data as the second parameter, in the form of python a dictionary.

To make it clear, let's assume we have the following component:

components/alert.html
<div class="alert alert-{{ type }}">
    {{ message }}
</div>

As you can see, the component is waiting for two variables: type and message. We can provide them in the template where we want to use it like this:

templates/home.html
<pb-alert type="success" message="Operation completed successfully."/>

or:

templates/home.html
@component('alert', {'type':'success', 'message':'Operation completed successfully.'})

The rendered output will look like this:

<div class="alert alert-success">
    Operation completed successfully.
</div>

Normal vs Bound attributes

When using PyBlade's component tags (<pb-component-name>), you can pass data as Normal HTML attributes (without :) or Bound attributes (starting with :). Both methods serve different purposes in how data is interpreted and passed to the component.

1. Normal attributes (Static values)

Normal attributes are passed as static strings. These values are not evaluated as Python expressions but are used as they are.

For example:

<pb-alert type="success" message="Operation completed successfully." />

Here, "success" and "Operation completed successfully." are passed as plain strings. The component receives them as-is, without any evaluation.

2. Bound Attributes (: Prefix for dynamic values)

When an attribute starts with :, it is treated as a Python expression, meaning it is evaluated before being passed to the component.

For example, assuming we have a variable status with a dynamic string value, we may pass it to the component by prefixing it with the : character like this:

<pb-alert :type="status" message="Operation done." />

In this example, :type="status" passes the value of status instead of the string "status".

The @props Directive

The @props directive in PyBlade allows you to define default values for properties (variables) within a component. This is particularly useful when creating reusable components where some properties might be optional. If a value is not provided when the component is used, it will automatically fallback to the default value specified in the @props directive.

The @props directive accepts a dictionary where the keys represent the names of the properties (variables) that the component expects, while the values represent the default values that will be used if no explicit value is provided when using the component.

For example, consider the following alert component:

components/alert.html
@props({'type':'info', 'message':'Default message'})

<div class="alert alert-{{ type }}">
    {{ message }}
</div>

When using this alert component, you can either pass a custom type and message attributes, or let the component fall back to its default values.

templates/home.html
<pb-alert message="User created successfully" />

Here, only the message property is provided as an attribute with the value "User created successfully", but the type is omitted. Because of this, the default value of type ("info") will be used.

The final rendered HTML will look like this:

<div class="alert alert-info">
    User created successfully
</div>

Since only message was specified, only the message text changed, but the type remains info resulting in the class name alert-info being applied.

Component attributes

We've already discussed passing declared data (props) to a component. But sometimes the caller needs to pass extra HTML attributes — like class, id, data-*, type, href — that aren't part of the component's declared props, but that should still land on the component's root element.

Any attribute passed on a component tag that isn't consumed by @props (or an expected variable) is automatically collected into a variable called attributes, available inside the component. Write it directly on the root element to apply all of it at once:

components/card.html
<div {{ attributes }}>
    <!-- Component content -->
</div>

For example, if you render:

templates/home.html
<pb-card class="mt-4" :user="user"/>

the class="mt-4" attribute isn't declared anywhere by the component, so it flows straight into attributes and gets printed on the root <div>.

attributes behaves like a plain, dict-like collection of whatever extra attributes were passed — you interact with it the same way you'd interact with any HTML attribute or any Python mapping.

Default values and automatic merging

Declare the attribute the way you normally would in HTML, alongside {{ attributes }}, and PyBlade treats your literal value as the default, merging or overriding it with whatever the caller passed — depending on the attribute.

components/card.html
<div class="card-item rounded-full" {{ attributes }}>
    {{ user.name }}
</div>

If you use the component like this:

templates/home.html
<pb-card class="mb-4" :user="user"/>

The final HTML rendered will be:

<div class="card-item rounded-full mb-4">
    <!-- Contents of the message variable -->
</div>

You wrote class="..." exactly like you would on any HTML element, and PyBlade combined it with the caller's class automatically.

Why class is special

class always combines rather than replaces, because that's what you almost always want with CSS classes: the component's base styling stays, and the caller's classes are appended. This happens automatically any time a literal class attribute sits on the same tag as {{ attributes }}.

Non-class attributes

For every other attribute, your literal value is a genuine default: the caller's value wins if they passed one, otherwise your literal stays as-is.

<button type="button" {{ attributes }}>
    {{ slot }}
</button>

If you use the component as follows:

<pb-button type="submit">
    Submit
</pb-button>

The rendered HTML will be:

<button type="submit">
    Submit
</button>

Had the caller not passed type at all, the button would have kept type="button".

Conditional classes

Write the condition inline, the same way you'd write any conditional expression inside an attribute in PyBlade:

<div class="p-4 {{ 'bg-red' if has_error else '' }}" {{ attributes }}>
    {{ message }}
</div>

{{ attributes }} still merges on top of whatever that expression produces, exactly as described above.

Reading a specific attribute

Since attributes is a mapping, read a value the same way you'd read any attribute or dict entry:

{{ attributes.class }}

Fall back to a default the same way you would with any Python value:

{{ attributes.class or 'default-class' }}

For attribute names that aren't valid Python identifiers (containing -, :, etc.), use item access instead of dot access:

{{ attributes['data-toggle'] }}

Checking if an attribute is present

Use plain truthiness or the in operator:

@if('class' in attributes)
    <div>Class attribute is present</div>
@endif

Checking for several attributes at once reads just like ordinary Python:

@if('name' in attributes and 'class' in attributes)
    <div>Both 'name' and 'class' attributes are present</div>
@endif

Checking that at least one of a few attributes exists:

@if('href' in attributes or ':href' in attributes)
    <div>One of the attributes is present</div>
@endif

Filtering attributes by prefix

attributes supports the same iteration you'd use on any dictionary, so filtering by prefix is a loop with a condition:

@for key, value in attributes.items() if key.startswith('data-')
    {{ key }}="{{ value }}"
@endfor

To exclude a prefix instead, flip the condition:

@for key, value in attributes.items() if not key.startswith('data-')
    {{ key }}="{{ value }}"
@endfor

Slots

Props let you pass data into a component — strings, numbers, booleans. But often what you want to hand a component isn't a value, it's markup: a paragraph, an icon, a button, a whole block of HTML built by the caller. That's what slots are for.

Picture a component's template as having one or more open spots reserved for the caller to fill in, the way a picture frame has an empty space waiting for a photo. The component decides where each spot sits and what surrounds it; the caller decides what goes inside it. Inside the component, a spot is just a variable you echo — slot for the main, unnamed content area, or a chosen name for any additional spot. Outside, whoever uses the component fills that spot simply by placing content between the component's opening and closing tags.

The default slot

Let's imagine an alert component with the following markup:

<div class="alert alert-danger">
    {{ slot }}
</div>

Here, {{ slot }} is the single, unnamed spot in this component. You fill it by placing content between the component tags:

<pb-alert>
    <strong>Whoops!</strong> Something went wrong!
</pb-alert>

Whatever sits between <pb-alert> and </pb-alert> becomes the value of slot inside the component.

Named slots

A component can offer more than one spot. Let's give our alert component a second spot for a title, in addition to its main content area:

<span class="alert-title">{{ title }}</span>

<div class="alert alert-danger">
    {{ slot }}
</div>

Fill a named spot using the <pb-slot> tag, matched by name. Anything left outside an explicit <pb-slot> tag still falls into the default, unnamed slot:

<pb-alert>
    <pb-slot name="title">
        Server Error
    </pb-slot>

    <strong>Whoops!</strong> Something went wrong !
</pb-alert>

This renders:

<span class="alert-title">Server Error</span>

<div class="alert alert-danger">
    <strong>Whoops!</strong> Something went wrong!
</div>

Pro tip When creating a named slot, you may pass the name using the colon notation, like this: <pb-slot:title> instead of using the attribute format <pb-slot name="title">.

Checking if a slot is empty

You can check if a slot contains content by using an @if statement with the is_empty method available on any slot object:

<span class="alert-title">{{ title }}</span>

<div class="alert alert-danger">
    @if(slot.is_empty())
        This is default content if the slot is empty.
    @else
        {{ slot }}
    @endif
</div>

Checking if a slot has actual content

Additionally, you can use the has_actual_content method to determine if the slot contains any "actual" content that is not an HTML comment:

@if(slot.has_actual_content())
    The scope has non-comment content.
@endif

Slot attributes

Just like the component's root element, a slot can receive its own extra attributes, such as CSS class names:

<pb-card class="shadow-sm">
    <pb-slot name="heading" class="font-bold">
        Heading
    </pb-slot>

    Content

    <pb-slot name="footer" class="text-sm">
        Footer
    </pb-slot>
</pb-card>

These are available through the attributes property of the slot's variable, and behave exactly like the component attributes described above.

On this page