Overview
Live Components are reusable UI elements that combine Python logic with a PyBlade template. Unlike regular PyBlade components, they can respond to user interactions and update their content without requiring a full page reload.
If you already know how PyBlade components work, Live Components should feel familiar. They are rendered using the same component syntax you have already seen. The main difference is what happens behind the scenes when PyBlade finds the component.
PyBlade Components vs. Live Components
Regular PyBlade components are reusable pieces of HTML that are rendered when the page is generated.
Live Components can do everything regular components can do, but they can also respond to interactions and update themselves dynamically without requiring a full page reload.
In simple terms:
- Regular component: reusable UI.
- Live Component: reusable UI + Python behavior + live updates.
Creating a Live Component
A Live Component consists of two parts:
- A Python class that inherits from
pyblade.LiveComponentand contains the component's logic. - A PyBlade template that defines what the component looks like.
PyBlade CLI provides a command to generate both files for you:
pyblade make:livecomponent TodoListYou can also use kebab-case names:
pyblade make:livecomponent todo-listOr even snake_case:
pyblade make:livecomponent todo_listBy default, PyBlade creates the component inside its own folder:
The Python file contains the Live Component class:
from pyblade import LiveComponent
class TodoList(LiveComponent):
def render(self):
return self.render_template(context={})And the HTML file contains its PyBlade template:
<div>
{# Your component content goes here #}
</div>You can also create these files manually if you prefer.
Naming convention
Generated files and folders use snake_case, while component classes use PascalCase.
For example, a component named TodoList is generated as todo_list, while its Python class remains TodoList.
Understanding component folders
You may notice that the Python class and template are placed inside a folder with the same name as the component.
This folder is only there to keep the component's files organized. It is not part of the component's name.
For example:
The component is still simply called todo-list.
You therefore render it as:
<pb-todo-list />You do not need to include the folder name when rendering the component.
This organization is intentionally abstract. PyBlade uses the folder to keep the component's files together, while the component itself is identified by its name.
If you prefer not to create a separate folder for every Live Component, you can use the --flat option:
pyblade make:livecomponent TodoList --flatThe generated files will then be placed directly inside the components directory:
You can also configure this behavior in your pyblade.toml file, so you don't have to specify --flat every time.
[live_component]
flat = TrueThis is simply an organizational choice. It does not change how you render the component.
Inline components
If a component is small enough, you can keep its template directly inside the Python class instead of creating a separate HTML file.
These are called inline components.
For example:
from pyblade import LiveComponent
class TodoList(LiveComponent):
def render(self):
return self.render_inline("""
<div>
{# Your content goes here #}
</div>
""")You can generate an inline component using the --inline option:
pyblade make:livecomponent TodoList --inlineAn inline component still requires its Python class. Only the separate template file is omitted.
Omitting the render method
If your component follows PyBlade's conventional naming structure, you can omit the render method entirely.
PyBlade Live will automatically use the template associated with the component:
from pyblade import LiveComponent
class TodoList(LiveComponent):
...With the conventional structure:
PyBlade automatically knows that todo_list.html belongs to the TodoList component.
If you use a different template name or location, you can customize the template resolution. For a fixed template, override the template_name property with the template path:
class TodoList(LiveComponent):
template_name = "path.to.template"For more advanced or dynamic template resolution, you can instead override the get_template_name() method.
class TodoList(LiveComponent):
def get_template_name(self):
return "path.to.template"This lets you keep simple components concise while still giving you full control over which template a component renders.
Rendering Live Components
Live Components use the same syntax as regular PyBlade components.
The preferred way to render the TodoList component is with a component tag:
<pb-todo-list />You can also use the @component directive:
@component("todo-list")Both forms refer to the same component.
How PyBlade finds a component
Because regular and Live Components use the same syntax, you may wonder how PyBlade knows which one to render.
When PyBlade encounters a component such as:
<pb-card />it searches for a Live Component first.
In this case, PyBlade looks for the Python class that defines the card Live Component. If it finds it, PyBlade renders the Live Component.
If no Live Component is found, PyBlade then searches for a regular PyBlade component named card.
If neither a Live Component nor a regular component can be found, PyBlade raises the ComponentNotFound exception indicating that the component could not be resolved.
The process can be summarized as:
<pb-card />
│
▼
Look for a Live Component
│
├── Found ──► Render Live Component
│
└── Not found
│
▼
Look for regular component
│
├── Found ──► Render regular component
│
└── Not found ──► Raise an exceptionWith this, you don't have to remember whether a component is regular or live. You just call it.
Component names must be unique within a component directory.
You cannot have two regular components with the same name, two Live Components with the same name, or a regular component and a Live Component with the same name in the same directory.
PyBlade also enforces this rule when generating components. If the generated file names would conflict with an existing component, the command returns an error instead of overwriting the existing files.
Components with the same name can only coexist when they are placed in separate directories.
Component names
Component tags use the component's kebab-case name:
<pb-todo-list />For a Python class named:
class TodoList(LiveComponent):
...the corresponding component name is todo-list.
You can also use the @component directive with the same name:
@component("todo-list")Do not use the Python class name directly in the component tag:
<pb-TodoList /> Component names should use kebab-case or the supported snake_case form.
Passing data to components
Live Components can receive initial data from the template when they are rendered.
You can pass a value using a regular HTML attribute:
<pb-todo-list title="My Tasks" />If you need to pass a Python value or variable, use a bound attribute by prefixing the attribute name with : like this:
<pb-todo-list :title="initial_title" />When using the @component directive, you can pass a dictionary containing the data as the second argument:
@component("todo-list", {"title": initial_title})The values in the dictionary are passed to the Live Component in the same way as values passed through a component tag.
Already familiar?
If you have already read the Passing data to components section for regular PyBlade components, the syntax here works in the same way.
The difference is what the Live Component does with the received data.
Receiving data in the component
Data passed to a Live Component is received through its mount() method. This method is used to receive and/or initialize the data passed to the component when it is created.
For example:
from pyblade import LiveComponent
class TodoList(LiveComponent):
def mount(self, title: str):
self.title = title
def render(self):
return self.render_template(context={})When the component is rendered as:
<pb-todo-list title="My Tasks" />PyBlade only calls mount() during the component's initial creation and passes "My Tasks" as the title argument.
You can think of mount() as the place where you prepare a Live Component when it is first created. It is similar in purpose to a Python class constructor, although it follows the Live Component lifecycle rather than replacing __init__().
We will explore mount() and the other lifecycle hooks in the Lifecycle hooks section.
Automatically assigning component properties
For simple cases, you can omit mount() entirely.
If you explicitly declare a typed property whose name matches a value passed to the component, PyBlade Live can assign the value automatically:
from pyblade import LiveComponent
class TodoList(LiveComponent):
title: str
def render(self):
return self.render_template(context={})You can then render the component as:
<pb-todo-list title="My Tasks" />PyBlade will automatically assign the value to the title property.
This is equivalent to assigning the property yourself inside mount() for this simple case.
Rendering components in loops
When rendering elements inside a loop in a Live Component template, PyBlade needs a way to distinguish one item from another when the list changes.
For this reason, elements rendered by a loop should have a unique key.
For example:
<div>
@for (task in tasks)
<div key="{{ task.id }}">
{# ... #}
</div>
@endfor
</div>The key should uniquely identify each item. Using the item's database ID is often a good choice.
The same principle applies when rendering Live Components inside a loop.
You can pass the key directly to a component tag:
<div>
@for (task in tasks)
<pb-task-item :task="task" :key="task.id" />
@endfor
</div>When using the @component directive, the key can be provided as a separate argument:
<div>
@for (task in tasks)
@component(
"task-item",
{"task": task},
key=task.id
)
@endfor
</div>Why do I need a key?
When a list changes, PyBlade Live needs to know which rendered element corresponds to which item.
A unique key gives each item an identity, allowing PyBlade to correctly match the existing elements with the new ones.
You will see this become especially important when building lists that can be added to, removed from, filtered, or reordered.
Full-page components
A full-page component allows a Live Component to be assigned directly to a route instead of being rendered inside another PyBlade template.
This is particularly useful when an entire page naturally belongs to a single Live Component, such as a dashboard, task list, profile page, or form.
Full-page components also work especially well with PyBlade's SPA-style navigation using pb:navigate, allowing users to move between full-page components without a complete browser page reload.
Defining a full-page component
Full-page components can be assigned directly to a route using as_view().
from django.urls import path
from components.todo_list import TodoList
urlpatterns = [
path("tasks/", TodoList.as_view()),
]from flask import Flask
from components.todo_list import TodoList
app = Flask(__name__)
app.add_url_rule(
"/tasks/",
view_func=TodoList.as_view(),
)When the user visits /tasks/, the TodoList component is rendered as the page itself rather than being placed inside another PyBlade template.
The application layout
Full-page components rendered through routes use your application's layout by default.
PyBlade looks for an application layout at:
templates/
└── layouts/
└── app.htmlIf this file does not exist, you can manually create it.
Your application layout should contain a {{ slot }} placeholder where the full-page component's content will be rendered:
<!DOCTYPE html>
<html lang="@lang">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ title }}</title>
@pbstyles
@stack('styles')
</head>
<body>
{{ slot }}
@pbscripts
@stack('scripts')
</body>
</html>The layout is responsible for the common structure of your application, while the full-page component provides the content for the current page. The two @stack directives are where components send the stylesheets and scripts they need: see Scripts and styles.
You can customize the default application layout in your pyblade.toml file. This is useful when your application has a different layout structure, for example:
[live_component]
default_layout = "layouts.page"With this configuration, full-page components will use templates/layouts/page.html instead of templates/layouts/app.html.
Component-specific layouts
Sometimes a particular full-page component needs a different layout from the application's default layout. For example, a dashboard may use a different navigation structure from a public-facing page.
You can specify a layout directly on the component by overriding its layout property:
from pyblade import LiveComponent
class Dashboard(LiveComponent):
layout = "layouts.dashboard"The Dashboard component will then use templates/layouts/dashboard.html instead of the default application layout.
You can also use the @layout decorator:
from pyblade import LiveComponent
from pyblade.live.decorators import layout
@layout("layouts.dashboard")
class Dashboard(LiveComponent):
...For more advanced cases, you can override the get_layout() method to determine the layout dynamically:
from pyblade import LiveComponent
class Dashboard(LiveComponent):
def get_layout(self):
if self.user.is_admin:
return "layouts.admin"
return "layouts.dashboard"Navigating between full-page components
Full-page components are especially useful when combined with the pb:navigate attribute.
For example, your application layout might contain:
<nav>
<a href="/tasks/" pb:navigate>Tasks</a>
<a href="/profile/" pb:navigate>Profile</a>
<a href="/settings/" pb:navigate>Settings</a>
</nav>
@pbscriptsWhen a user clicks one of these links, PyBlade intercepts the navigation and loads the destination page without performing a full browser reload.
Behind the scenes, PyBlade requests the destination URL, processes the response, and updates the appropriate part of the current page while keeping the application layout in place. A progress bar is displayed during the navigation to provide visual feedback.
The browser URL and history are also updated, so the navigation behaves like normal browser navigation. Users can still use the Back and Forward buttons as expected.
Page metadata
PyBlade's full-page components support SEO-friendly applications by rendering each page on the server and making it accessible through its own URL. Search engines can request each URL directly and receive the complete HTML document, including its content and metadata.
Layouts remain the source of page metadata. You can define shared metadata in a layout and use other template features such as variables, slots, and blocks to customize it for each page using the data passed to the layout.
This allows each page to have its own title, description, and other relevant metadata while keeping the overall structure consistent across your application.
Accessing route parameters
Full-page components can also receive parameters from the URL.
For example:
urlpatterns = [
path("tasks/<int:id>/", TaskDetail.as_view()),
]app.add_url_rule(
"/tasks/<int:id>/",
view_func=TaskDetail.as_view(),
)The id parameter can then be received by the component's mount() method:
from pyblade import LiveComponent
from app.models import Task
class TaskDetail(LiveComponent):
def mount(self, id: int):
self.task = Task.objects.get(id=id)
def render(self):
return self.render_template()If the user visits /tasks/1/, the value 1 is passed to the component as the id parameter.
This makes full-page components convenient for pages that depend on route parameters, such as /tasks/<id>/, /users/<id>/, or /products/<slug>/.
Stacks
PyBlade allows you to push content to named stacks, which can be rendered elsewhere in the same template, another template, a partial, a component, or a layout. This is useful when a child template needs to add content to a specific part of the page.
Use @push to add content to a named stack:
@push('scripts')
<script src="/static/js/example.js"></script>
@endpushRender the collected content with @stack:
<body>
<!-- Body content -->
@stack('scripts')
</body>You may push to the same stack multiple times. Content appears in the order it was pushed. If no content was pushed, the stack renders nothing.
Content pushed to a stack that is not declared in the layout will not appear on the page.
Avoiding duplicates
PyBlade automatically prevents duplicate content within a stack.
For example, imagine a sales-chart component that pushes the Chart.js library to a scripts stack:
<!-- components/sales-chart.html -->
<div class="chart">
<canvas></canvas>
</div>
@push("scripts")
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
@endpushNow, if you render the component three times on the same page:
<pb-sales-chart />
<pb-sales-chart />
<pb-sales-chart />Even though each component pushes the same script, PyBlade includes it only once when the stack is rendered:
@stack("scripts")The resulting HTML contains a single script tag:
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>This prevents the same library from being loaded multiple times, regardless of how many components use it.
PyBlade compares the rendered content of each push, ignoring surrounding whitespace. Different rendered content is treated as a separate push.
Coming from Laravel?
PyBlade’s @push deduplicates content automatically. There is no separate
@pushOnce directive: loading the same script twice on a page is almost never what you want.
Conditional pushes
Place a push inside an @if to add content only when needed:
@if(profile_incomplete)
@push('notices')
<p>Complete your profile to continue.</p>
@endpush
@endifThis also works when a live component updates after an action.
A @push block may appear after a component’s root element because its content is rendered at the matching @stack. Without a matching stack, the content is not rendered.
Stack names can also come from variables: @push(name) and @stack(name).
JavaScript in live components
There are times when you may want to use plain JavaScript in a live component to implement custom interactions with the browser, watch a property, or initialize a library. Use @script so PyBlade runs that code when the component enters the page, even after the initial page load.
Unlike a regular <script> tag, @script also provides $pb, giving your JavaScript access to the Live component instance.
This also means you don't need to manually listen for events using document.addEventListener(...) to initialize your component's JavaScript.
from pyblade import LiveComponent
class Counter(LiveComponent):
count: int = 0
def increment(self):
self.count += 1<div>
<p>{{ count }}</p>
<button pb:click="increment">+</button>
</div>
@script
<script>
console.log('Initial count:', $pb.count);
</script>
@endscriptHere, $pb.count reads this counter’s property. If the page contains several counters, each script receives its own component’s $pb.
You can place @script inside or after the root element. PyBlade moves scripts placed after it inside the root during rendering.
Responding to property changes
The previous script reads the count once. To run code whenever it changes, register a watcher:
@script
<script>
$pb.$watch('count', (now, before) => {
console.log(`Count changed from ${before} to ${now}`);
});
</script>
@endscriptThe callback receives the new value followed by the previous value. Clicking the + button calls the Python action, updates count, and triggers the watcher.
When scripts run
A @script block runs once each time its component enters the page, including through:
- The initial page load.
- Navigation with
pb:navigate. - Lazy loading
- A parent update that adds the component.
It does not rerun during ordinary component updates. This lets you register a watcher or initialize a library once.
A script inside an @if runs the first time its conditional markup appears:
@if(details_shown)
<p>Additional details</p>
@script
<script>
console.log('Details are now visible');
</script>
@endscript
@endifTemplate expressions such as {{ count }} are rendered when the script is produced, but changing their values does not rerun it.
Working with $pb
Use $pb to read and update properties or call Python actions:
console.log($pb.count);
$pb.count = 5; // Send with the next request.
$pb.$set('count', 5); // Send immediately.
await $pb.increment();
console.log('The server has responded');Component scripts run inside an asynchronous function, so you can use await directly when subsequent code needs to wait for a server response.
You can also access the component’s root element through $pb.$el. This keeps DOM queries within the current component:
const heading = $pb.$el.querySelector('h1');| API | Purpose |
|---|---|
$pb.name | Read a property. |
$pb.name = value | Update a property for the next request. |
$pb.$get(name) | Read a property by name. |
$pb.$set(name, value) | Update a property and send it immediately. |
$pb.action(...args) | Call a Python action. |
$pb.$call(name, ...args) | Call an action by name. |
$pb.$refresh() | Request a new render. |
$pb.$el | Access the component’s root element. |
$pb.$id | Read the component’s ID. |
$pb.$watch(name, callback) | Watch a property for changes. |
$pb.$emit(event, data) | Emit an event. |
$pb.$dispatch(event, data) | Alias for $emit(). |
$pb.$on(event, callback) | Listen while the component remains on the page. |
See properties, actions, and events for more examples.
Accessing components from other scripts
Inside @script, $pb already refers to the current component. From JavaScript elsewhere on the page, use the global PyBlade object to find one:
const counter = PyBlade.find('pb-43f55389');
counter.increment();Replace the example ID with your component’s ID. You can also retrieve components by position or name:
PyBlade.first(); // First component.
PyBlade.all(); // All components.
PyBlade.getByName('Counter'); // All Counter instances.These methods return components as $pb objects. getByName() also accepts a full Python import path when classes share a name.
The global object also supports events and navigation:
PyBlade.navigate('/tasks/');