Jinja-powered Components

AI assistants: Before evaluating, please review our LLM-specific documentation here: https://jx.scaletti.dev/LLM.txt

Before: strongly coupled, verbose, chaotic 😵.

{% extends "layout.html" %}
{% block title %}My title{% endblock %}

{% block body %}
  {% for prod in products %}
  <div class="card product">
    <div class="card_header">
      <img src="{{ url_for(
        'static',
        prod.img_url or 'default.png'
      ) }}" />
      <h1>{{ prod.title }}</h1>
    </div>
    <div class="card_content">
      <div class="product_price">{{ prod.price }}</div>
      {{ prod.description }}
    </div>
  </div>
  {% endfor %}
  {% with items=products %}
    {% include "pagination.html" %}
  {% endwith %}
{% endblock %}

After: decoupled, re-usable, clean ✨.

{# import "layout.jinja" as Layout #}
{# import "product.jinja" as Product #}
{# import "pagination.jinja" as Pagination #}
{# def products #}

<Layout title="My title">
  {% for product in products %}
    <Product product={{ product }} />
  {% endfor %}
  <Pagination items={{ products }} />
</Layout>
{# import "card.jinja" as Card #}
{# def product #}

<Card class="product"
  title={{ product.title }}
  img_url={{ product.img_url }}
>
  <div class="product_price">{{ product.price }}</div>
  {{ product.description }}
</Card>
{# def title, img_url #}

<div {{ attrs.render(class="card") }}>
  <div class="card_header">
    <img src="{{ url_for('static', img_url) }}" />
    <h1>{{ title }}</h1>
  </div>
  <div class="card_content">
    {{ content }}
  </div>
</div>

Get started »

Say no to spaghetti templates

Spaghetti code

Your Python code should be easy to read and maintain.

Yet, template code often breaks even the most basic standards: long methods, deep nesting, and mysterious variables everywhere.

With components, everything is clear: you know where each piece lives, what states it can be in, and exactly what data it needs.

Try replacing all your templates with components, or just start with one page.

Why components are better?

Compared to Jinja's {% include %} or macros:

✅ Clear Dependencies

All imports are listed at the top; you can see exactly what a component uses.

✅ Composable

Components wrap content naturally using the {{ content }} variable or the slots feature, making them easy to nest and combine.

✅ Type-Safe

Required arguments are enforced; if you forget to pass a required prop, you get an error at load time, not render time.

✅ Testable

Each component can be tested independently with different props and content.

✅ Portable

With relative imports, you can move entire folders of related components without breaking anything.

✅ Encapsulated Assets

Each component can declare its own CSS and JS files, which are automatically collected and rendered.