Catalog
The Catalog is the central manager for your components. It handles loading, caching, and rendering components from one or more folders.
Basic Setup
from jx import Catalog
catalog = Catalog("components/")
This creates a catalog that loads components from the components/ folder.
Constructor Options
catalog = Catalog(
folder="components/", # Optional initial folder
jinja_env=None, # Custom Jinja2 environment
extensions=None, # Extra Jinja2 extensions
filters=None, # Custom template filters
tests=None, # Custom template tests
auto_reload=True, # Auto-detect file changes
asset_resolver=None, # Asset URL resolver callback
file_ext=".jx", # Component file extension
bytecode_cache=None, # Keep compiled components between runs
**globals # Global template variables
)
folder
Optional path to a component folder. Shortcut for calling add_folder():
# These are equivalent:
catalog = Catalog("components/")
catalog = Catalog()
catalog.add_folder("components/")
file_ext
The extension Jx uses to discover component files. Defaults to .jx.
Set it to keep a different convention (for example .jinja for older projects):
catalog = Catalog("components/", file_ext=".jinja")
auto_reload
When True (default), Jx checks if component files have changed and reloads them automatically. Great for development.
For production, set to False to skip file modification checks:
catalog = Catalog("components/", auto_reload=False)
bytecode_cache
Compiling a component is most of the cost of loading it, and Jx already keeps
every compiled component in memory for the life of the process. Pass a
jinja2.BytecodeCache to keep them between processes as well:
import jinja2
catalog = Catalog(
"components/",
auto_reload=False,
bytecode_cache=jinja2.FileSystemBytecodeCache(".jx-cache"),
)
The first run compiles and writes; later runs read what is already there. On the 21 components of this documentation site, start-up goes from 31 ms to 4 ms.
This only pays off where a process boundary is crossed:
- a development server that restarts on every change
- a deploy, or a worker that did not fork from an already warm parent
- short-lived processes: serverless, CI, a CLI that renders once
In a long-running single process it changes nothing, because the in-memory cache already answers every request after the first.
Any jinja2.BytecodeCache works, including MemcachedBytecodeCache, which
lets several workers or machines share one cache.
An entry is dropped when the component changes, and also when anything that
changes generated code changes: the Jinja version, autoescape, the extension
list, or the delimiters. You do not have to clear the cache by hand after an
upgrade.
Setting bytecode_cache on a jinja_env you pass in works too. Before this
option existed it was silently ignored, because Jinja only consults it from a
template loader and Jx does not use one.
globals
Variables available to all components:
catalog = Catalog(
"components/",
site_name="My App",
current_year=2026,
debug=True,
)
<footer>
© {{ current_year }} {{ site_name }}
</footer>
filters
Custom Jinja2 filters:
def format_price(value):
return f"${value:,.2f}"
def pluralize(count, singular, plural=None):
plural = plural or f"{singular}s"
return singular if count == 1 else plural
catalog = Catalog(
"components/",
filters={
"price": format_price,
"pluralize": pluralize,
}
)
<span>{{ product.price | price }}</span>
<span>{{ count }} {{ count | pluralize("item") }}</span>
tests
Custom Jinja2 tests:
def is_admin(user):
return user.role == "admin"
catalog = Catalog(
"components/",
tests={"admin": is_admin}
)
{% if user is admin %}
<a href="/admin">Admin Panel</a>
{% endif %}
extensions
Extra Jinja2 extensions to load:
catalog = Catalog(
"components/",
extensions=["jinja2.ext.i18n", "jinja2.ext.loopcontrols"]
)
Note: The jinja2.ext.do extension is always enabled (required for attrs manipulation).
jinja_env
Use an existing Jinja2 environment instead of creating a new one:
from jinja2 import Environment
env = Environment()
env.globals["my_func"] = my_function
env.filters["my_filter"] = my_filter
catalog = Catalog("components/", jinja_env=env)
This is useful when integrating with frameworks that provide their own Jinja environment.
asset_resolver
Optional callback for transforming component asset URLs. Receives (url, prefix) and returns the resolved URL string. Only invoked for components whose prefix has a registered assets folder (see add_folder).
def my_resolver(url, prefix):
return f"/static/{prefix}/{url}"
catalog = Catalog("components/", asset_resolver=my_resolver)
Adding Folders
add_folder(path, prefix="", assets=None)
Add a folder of components to the catalog:
catalog = Catalog()
catalog.add_folder("components/")
catalog.add_folder("layouts/")
The optional assets parameter specifies a folder containing CSS/JS files for components in this folder. When set, the asset_resolver callback is used to transform asset URLs at render time (see Installable Packages for details).
Components are imported by their path relative to the folder:
{#import "button.jx" as Button #}
{#import "forms/input.jx" as Input #}
Using Prefixes
Prefixes namespace components, useful for third-party libraries:
catalog.add_folder("components/")
catalog.add_folder("vendor/ui-kit/", prefix="ui")
catalog.add_folder("vendor/icons/", prefix="icons")
Import prefixed components with @prefix/:
{#import "button.jx" as Button #}
{#import "@ui/modal.jx" as Modal #}
{#import "@icons/check.jx" as CheckIcon #}
Multiple Folders, Same Prefix
If you add multiple folders with the same prefix (or no prefix), they're treated as one namespace. If both contain a component with the same path, the first one added wins:
catalog.add_folder("my-components/") # Has button.jx
catalog.add_folder("fallback-components/") # Also has button.jx
# "button.jx" resolves to my-components/button.jx
Rendering
render(relpath, globals=None, **kwargs)
Render a component by its path:
html = catalog.render("page.jx", title="Hello", user=current_user)
Arguments:
relpath- Path to the component (e.g.,"pages/home.jx")globals- Dict of variables available to this component and all its imports**kwargs- Arguments passed directly to the component
# Pass data as keyword arguments
html = catalog.render(
"user-profile.jx",
user=user,
posts=posts,
show_email=True,
)
# Or use globals for values needed by child components too
html = catalog.render(
"page.jx",
globals={"request": request, "csrf_token": token},
title="Dashboard",
)
render_string(source, globals=None, **kwargs)
Render a component from a string (not a file):
source = """
{#def name #}
<h1>Hello, {{ name }}!</h1>
"""
html = catalog.render_string(source, name="World")
# <h1>Hello, World!</h1>
Useful for:
- Testing components
- Dynamic templates from a database
- Simple one-off renders
Note: String components can use absolute imports but not relative imports (no file path to resolve from).
Introspection
list_components()
Returns a list of all registered component paths:
paths = catalog.list_components()
# ["button.jx", "card.jx", "forms/input.jx"]
get_signature(relpath)
Returns a component's signature, including its arguments and metadata:
sig = catalog.get_signature("button.jx")
Returns a dictionary with:
required- dict of required argument names mapped to their type (orNone)optional- dict of optional arguments mapped to(default_value, type or None)slots- tuple of slot namescss- tuple of CSS file URLsjs- tuple of JS file URLs
sig = catalog.get_signature("modal.jx")
# {
# "required": {"title": str},
# "optional": {"size": ("md", str)},
# "slots": ("header", "footer"),
# "css": ("modal.css",),
# "js": ("modal.js",),
# }
collect_assets(output)
Copies all registered package assets to an output folder. For each prefix that has a registered assets folder (see add_folder), files are copied to <output>/<prefix>/:
copied = catalog.collect_assets("static/vendor")
# [("ui", Path("button.css")), ("ui", Path("button.js")), ...]
Returns a list of (prefix, relative_path) tuples for every file copied.
Framework Integration
Flask
from flask import Flask, url_for
from jx import Catalog
app = Flask(__name__)
catalog = Catalog(
"components/",
auto_reload=app.debug,
url_for=url_for, # Make url_for available in components
)
@app.route("/")
def home():
return catalog.render("pages/home.jx", products=get_products())
FastAPI
from fastapi import FastAPI, Request
from fastapi.responses import HTMLResponse
from jx import Catalog
app = FastAPI()
catalog = Catalog("components/", auto_reload=True)
@app.get("/", response_class=HTMLResponse)
def home(request: Request):
return catalog.render(
"pages/home.jx",
globals={"request": request},
products=get_products(),
)
Sharing Jinja Environment
If your framework has its own Jinja environment with filters, globals, etc., pass it to the catalog:
# Flask example
from flask import Flask
app = Flask(__name__)
app.jinja_env.filters["my_filter"] = my_filter
app.jinja_env.globals["my_global"] = my_global
catalog = Catalog("components/", jinja_env=app.jinja_env)
Now your components have access to everything registered in Flask's environment.
Production Settings
For production, disable auto-reload.
import os
catalog = Catalog(
"components/",
auto_reload=os.environ.get("DEBUG", "false").lower() == "true",
)
Or based on your framework's debug setting:
# Flask
catalog = Catalog("components/", auto_reload=app.debug)
# FastAPI
catalog = Catalog("components/", auto_reload=settings.debug)
Built-in Template Globals
In addition to any globals you pass to the constructor, Jx automatically provides these functions to all components:
_get_random_id(prefix="id")
Generates a unique string suitable for HTML element IDs. Useful for form elements, popovers, and other components that require unique IDs to function correctly:
{#def label, content #}
{% set popover_id = _get_random_id("popover") %}
<button popovertarget="{{ popover_id }}">{{ label }}</button>
<div id="{{ popover_id }}" popover>{{ content }}</div>
Each call returns a different ID like popover-a1b2c3d4e5f6..., so you can use it as a default without requiring the caller to pass an explicit ID:
{#def name, label="", id="" #}
{% set input_id = id or _get_random_id(name) %}
{% if label %}
<label for="{{ input_id }}">{{ label }}</label>
{% endif %}
<input id="{{ input_id }}" name="{{ name }}" {{ attrs.render() }} />