Components
A component is a reusable template snippet that works like a function. It can take arguments, render content, and be composed with other components to build complex UIs.
Think of components as the building blocks of your interface; buttons, cards, forms, layouts; anything you use more than once or want to keep organized.
Creating a Component
Components are Jinja template files with a .jinja extension:
{#def text #}
<button class="btn">{{ text }}</button>
Anatomy of a Component
A complete component can have these parts:
{#import "./header.jinja" as Header #}
{#css card.css #}
{#js card.js #}
{#def title, subtitle="" #}
<div class="card">
<Header title={{ title }} subtitle={{ subtitle }} />
<div class="card-body">
{{ content }}
</div>
</div>
From top to bottom:
- Imports - Other components this one uses
- Assets - CSS and JS files
- Arguments - Data the component accepts
- Template - The HTML to render
All parts are optional except the template.
Using Components
Import a component, then use it like an HTML tag:
{#import "button.jinja" as Button #}
{#import "card.jinja" as Card #}
<Card title="Welcome">
<p>Hello world!</p>
<Button text="Click me" />
</Card>
Block syntax for components with content:
<Card title="Hello">
<p>Content goes here</p>
</Card>
Self-closing syntax for components without content:
<Button text="Click me" />
Imports
Jx requires explicit imports before using components.
Basic Syntax
{#import "path/to/component.jinja" as Name #}
File Naming
Component files and folders can use any naming convention: button.jinja, user-card.jinja, form_input.jinja, etc. The import alias, however, must be PascalCase to distinguish components from HTML:
{#import "user-card.jinja" as UserCard #}
{#import "form_input.jinja" as FormInput #}
<UserCard /> {# Component #}
<div /> {# HTML #}
Absolute Imports
Paths relative to a catalog folder:
{#import "components/button.jinja" as Button #}
{#import "layouts/base.jinja" as Base #}
Use for shared components used across your project.
Relative Imports
Paths relative to the current file:
{#import "./sibling.jinja" as Sibling #}
{#import "../parent/component.jinja" as Component #}
{#import "./subfolder/child.jinja" as Child #}
Use for tightly related components. Move an entire folder and internal imports still work.
Example structure:
components/
modal/
modal.jinja {#import "./header.jinja" as Header #}
header.jinja {#import "./close-btn.jinja" as CloseBtn #}
close-btn.jinja
Prefixed Imports
For components from prefixed catalog folders:
catalog.add_folder("vendor/ui-lib", prefix="ui")
{#import "@ui/button.jinja" as Button #}
{#import "@ui/modal.jinja" as Modal #}
Use for third-party component libraries.
Arguments
Components accept arguments to customize their behavior.
Declaring Arguments
Use {#def ... #} at the top of your component:
{#def title, count=0 #}
<h2>{{ title }}: {{ count }}</h2>
title- Required (no default)count- Optional (defaults to0)
Type Annotations
Arguments can also have type annotations:
{#def
title: str,
count: int = 0,
items: list[str] = [],
data: dict[str, int] = {}
#}
For primitive types (int, str, bool, list, dict, etc.), Jx validates at runtime that passed arguments match the declared types.
However, this is limited to simple types. For example, for arguments with types like list[str], Jx only checks that the argument is a list, not that its elements are
strings. The same is true for dict[str, int], tuple[int, ...], etc. It also cannot check any union types like int | str.
Default Values
Defaults can be any Python value: strings, numbers, booleans, lists, dicts, etc.
{#def
name,
count=0,
active=true,
items=[],
config={}
#}
Passing Arguments
Strings use quotes:
<Button text="Click me" />
Expressions use {{ }}:
<Card
user={{ current_user }}
count={{ items | length }}
active={{ true }}
items={{ [1, 2, 3] }}
/>
Booleans can use HTML-style shorthand for true:
<Input required /> {# Same as required={{ true }} #}
<Input disabled={{ false }} />
Dash to Underscore
Dashes in attribute names convert to underscores:
{#def aria_label, data_id #}
<Button aria-label="Close" data-id="123" />
Both aria-label and aria_label match aria_label in the def.
Content & Slots
Components can wrap content passed between their tags.
The content Variable
Everything between a component's tags is available as content:
{#def title #}
<div class="card">
<h3>{{ title }}</h3>
<div class="body">{{ content }}</div>
</div>
<Card title="Hello">
<p>This becomes the content!</p>
</Card>
Fallback Content
Provide defaults when no content is passed:
<div class="body">
{{ content or "No content provided" }}
</div>
Named Slots
For multiple content areas, use named slots.
Define slots in the component with {% slot %}:
<div class="modal">
<div class="modal-header">
{% slot header %}
<h3>Default Header</h3>
{% endslot %}
</div>
<div class="modal-body">
{{ content }}
</div>
<div class="modal-footer">
{% slot footer %}
<button>Close</button>
{% endslot %}
</div>
</div>
Fill slots when using the component with {% fill %}:
<Modal>
{% fill header %}
<h3>Confirm</h3>
{% endfill %}
<p>Are you sure?</p>
{% fill footer %}
<button>Yes</button>
<button>No</button>
{% endfill %}
</Modal>
Unfilled slots use their default content.
When to Use Slots vs Props
Use Props When:
- The content is a single value
- You want validation
{#def title, count #}
<h2>{{ title }}: {{ count }}</h2>
Use Content When:
- The content is HTML
- There's one main content area
- You want flexibility in what gets passed
{#def title #}
<div class="card">
<h2>{{ title }}</h2>
{{ content }}
</div>
Use Named Slots When:
- You need multiple content areas
- Each area has a specific purpose
- You might want to provide defaults for each area
<div class="panel">
<header>{% slot header %}Default{% endslot %}</header>
<main>{{ content }}</main>
<footer>{% slot footer %}Default{% endslot %}</footer>
</div>
Validation
You can easily validate your components using the CLI tool jx installs. Just run:
❯❯ jx check PATH_TO_FOLDER
Example:
❯❯ jx check docs/views/
✓ autodoc.md.jinja - OK
✓ banner_archived.jinja - OK
✓ color_scheme.jinja - OK
✓ footer.jinja - OK
✓ header.jinja - OK
✓ humans.txt.jinja - OK
✓ index.jinja - OK
✓ language_selector.jinja - OK
✓ layout.jinja - OK
✓ llm.jinja - OK
✓ metadata.jinja - OK
✓ page.jinja - OK
✓ page_nav.jinja - OK
✓ page_pager.jinja - OK
✓ page_toc.jinja - OK
✓ robots.txt.jinja - OK
✓ search.jinja - OK
✓ sidebar.jinja - OK
✓ sitemap.xml.jinja - OK
✓ toc.jinja - OK
✓ version_selector.jinja - OK
21 components checked, 0 errors