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:
You can manually create a component inside the components folder, or use the provided command to generate one:
pyblade make:component alertThis command will create the file: components/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.
Where components are found
PyBlade looks for a component in your project's components/ folder (the folder set by paths.components).
In a Django project, an app can bring components of its own, in a components/ folder inside the app, next to its templates/. They are found after the project's, in the order your apps are listed in INSTALLED_APPS:
components/badge.html # 1. the project's own
apps/billing/components/badge.html # 2. then each app'sThis means:
- an app's components need nothing to be registered: if the app is installed, its components can be used from every template, with the same syntax as the project's,
- the project can replace a component an app provides by writing one with the same name in its own
components/folder, - live components work the same way, and a live component keeps its template next to its class, wherever it is.
pyblade make:component --app billing creates a component in an app. See creating files in a Django app.
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:
<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:
<pb-alert type="success" message="Operation completed successfully."/>or:
@component('alert', {'type':'success', 'message':'Operation completed successfully.'})The rendered output will look like this:
<div class="alert alert-success">
Operation completed successfully.
</div>Regular vs Bound attributes
When using PyBlade's component tags (<pb-component-name>), you can pass data as Regular HTML attributes (without :) or Bound attributes (starting with :).
Both methods serve different purposes in how data is interpreted and passed to the component.
Regular attributes
Regular 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.
Bound Attributes
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:
@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.
<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:
<div {{ attributes }}>
<!-- Component content -->
</div>For example, if you render:
<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.
<div class="card-item rounded-full" {{ attributes }}>
{{ user.name }}
</div>If you use the component like this:
<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 }}>
Click Me
</button>If you use the component as follows:
<pb-button type="submit" />The rendered HTML will be:
<button type="submit">
Click Me
</button>Had the caller not passed type at all, the button would have kept type="button".
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'] }}Slots
Props let you pass data to a component, such as strings, numbers, or booleans. But sometimes you want to pass content instead: a paragraph, an icon, a button, or even a whole block of HTML. That's what slots are for.
Think of a component as a template with one or more spaces that the caller can fill with content. The component decides where these spaces appear and what surrounds them, while the caller provides what goes inside.
Inside the component, each space is accessed through a variable: slot for the main, unnamed content, or a custom name for additional slots. When using the component, the caller fills these slots by placing content between the component's opening and closing tags.
The default slot
Let's imagine a card component with the following template:
<div class="card">
<h2>{{ title }}</h2>
<div class="card-content">
{{ slot }}
</div>
</div>Here, {{ slot }} is the component's default, unnamed slot. It represents the content provided by whoever uses the component.
You fill the default slot by placing content between the component's opening and closing tags:
<pb-card title="Welcome">
<p>This is the content of the card.</p>
</pb-card>Everything between <pb-card> and </pb-card> becomes the value of slot inside the component. PyBlade then renders that content wherever {{ slot }} appears in the component template.
The slot variable is not escaped by default because it is intended to contain rendered HTML. Only use slots with trusted or properly sanitized content. Passing untrusted user input directly into a slot can introduce cross-site scripting (XSS) vulnerabilities.
Named slots
A component can have more than one slot when it needs to accept different pieces of content.
For example, we can give our alert component a named title slot in addition to its default slot:
<div class="alert alert-{{ type }}">
<h2>{{ title }}</h2>
<div>
{{ slot }}
</div>
</div>The title variable represents the named slot, while slot remains the default, unnamed slot.
To fill a named slot, use the <pb-slot> tag with the corresponding name attribute:
<pb-alert type="danger">
<pb-slot name="title">Server Error</pb-slot>
<strong>Whoops!</strong> Something went wrong!
</pb-alert>The content inside <pb-slot name="title"> is assigned to the title slot. Any content outside an explicit <pb-slot> remains part of the default slot.
The result is:
<div class="alert alert-danger">
<h2>Server Error</h2>
<div>
<strong>Whoops!</strong> Something went wrong!
</div>
</div>Pro Tip
You can use the shorthand syntax <pb-slot:title> ... </pb-slot> instead of <pb-slot name="title"> ... </pb-slot> when defining a named slot.