# Jx ## Index 1. [Home](#index) 2. [Quickstart](#quickstart) 3. [Catalog](#catalog) 4. [Components](#components) 5. [Attrs](#attrs) 6. [Assets](#assets) 7. [Catalog class](#api-catalog) 8. [jx.Attrs class](#api-attrs) 9. [Layout Components](#recipes-layouts) 10. [SVG Icons](#recipes-icons) 11. [Working with Flask](#working-flask) 12. [Working with Django](#working-django) 13. [Working with FastAPI](#working-fastapi) 14. [Working with htmx](#working-htmx) 15. [Installable Packages](#installable) 16. [Claude Skill](#tools-skill) 17. [Validator](#tools-check) 18. [VSCode extension](#tools-vscode) 19. [Migrating from JinjaX to Jx](#from-jinjax) --- --- title: Home id: index view: index.jx url: / --- ::::::: div home__section home__code ::::: div **Before**: strongly coupled, verbose, chaotic ๐Ÿ˜ต. ::: tab | view.html ```html+jinja {% extends "layout.html" %} {% block title %}My title{% endblock %} {% block body %} {% for prod in products %}

{{ prod.title }}

{{ prod.price }}
{{ prod.description }}
{% endfor %} {% with items=products %} {% include "pagination.html" %} {% endwith %} {% endblock %} ``` ::: ::::: ::::: div **After**: decoupled, reusable, clean โœจ. ::: tab | view.jx ```html+jinja {#import "layout.jx" as Layout #} {#import "product.jx" as Product #} {#import "pagination.jx" as Pagination #} {#def products #} {% for product in products %} {% endfor %} ``` ::: ::: tab | product.jx ```html+jinja {#import "card.jx" as Card #} {#def product #}
{{ product.price }}
{{ product.description }}
``` ::: ::: tab | card.jx ```html+jinja {#def title, img_url #}

{{ title }}

{{ content }}
``` ::: ::::: ::::::: ::::: div home__section home__actions [Get started ยป](/docs/){ .btn .btn--primary } ::::: :::::: div bg ::::: div home__section home__spaghetti ## Say no to spaghetti templates ![Spaghetti code](/assets/images/spaghetti_code.png){ .left width="300" } 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. ::::: :::::: ::::: div home__section home__better ## Why are components 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. ::::: ---- --- title: Quickstart id: quickstart url: /docs/quickstart/ --- ## Install Jx Run the following command: ::: tab | Using "**pip**" ```bash pip install jx ``` ::: ::: tab | Using "**uv**" ```bash uv add jx ``` ::: ## Create a catalog ```python {title="app.py"} from jx import Catalog catalog = Catalog("components/") ``` ## Create a component Create a new folder for your components. Inside this folder create a new file called `card.jx` with the following content: ```html+jinja {title="components/card.jx"} {#def title, url #}

{{ title }}

{{ content }}

Read more
``` ## Use the component ```python {title="views.py"} from .app import catalog def dashboard_view(): return catalog.render("dashboard.jx") ``` ```html+jinja {title="components/dashboard.jx"} {#import "card.jx" as Card #} We have the best trees The best spades in the land ``` ::: tab | Preview

Trees

We have the best trees

Read more

Spades

The best spades in the land

Read more
::: ::: tab | HTML ```html

Trees

We have the best trees

Read more

Spades

The best spades in the land

Read more
``` ::: --- title: Catalog description: Configuring and using the Jx Catalog id: catalog url: /docs/catalog/ --- The `Catalog` is the central manager for your components. It handles loading, caching, and rendering components from one or more folders. ## Basic Setup ```python from jx import Catalog catalog = Catalog("components/") ``` This creates a catalog that loads components from the `components/` folder. ## Constructor Options ```python 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 **globals # Global template variables ) ``` ### `folder` Optional path to a component folder. Shortcut for calling `add_folder()`: ```python # 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): ```python 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: ```python catalog = Catalog("components/", auto_reload=False) ``` ### `globals` Variables available to all components: ```python catalog = Catalog( "components/", site_name="My App", current_year=2026, debug=True, ) ``` ```html+jinja title="components/footer.jx" ``` ### `filters` Custom Jinja2 filters: ```python 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, } ) ``` ```html+jinja {{ product.price | price }} {{ count }} {{ count | pluralize("item") }} ``` ### `tests` Custom Jinja2 tests: ```python def is_admin(user): return user.role == "admin" catalog = Catalog( "components/", tests={"admin": is_admin} ) ``` ```html+jinja {% if user is admin %} Admin Panel {% endif %} ``` ### `extensions` Extra Jinja2 extensions to load: ```python 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: ```python 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`). ```python 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: ```python 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](/docs/installable/) for details). Components are imported by their path relative to the folder: ```html+jinja {#import "button.jx" as Button #} {#import "forms/input.jx" as Input #} ``` ### Using Prefixes Prefixes namespace components, useful for third-party libraries: ```python catalog.add_folder("components/") catalog.add_folder("vendor/ui-kit/", prefix="ui") catalog.add_folder("vendor/icons/", prefix="icons") ``` Import prefixed components with `@prefix/`: ```html+jinja {#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**: ```python 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: ```python 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 ```python # 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): ```python source = """ {#def name #}

Hello, {{ name }}!

""" html = catalog.render_string(source, name="World") #

Hello, World!

``` 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: ```python paths = catalog.list_components() # ["button.jx", "card.jx", "forms/input.jx"] ``` ### `get_signature(relpath)` Returns a component's signature, including its arguments and metadata: ```python sig = catalog.get_signature("button.jx") ``` Returns a dictionary with: - `required` - dict of required argument names mapped to their type (or `None`) - `optional` - dict of optional arguments mapped to `(default_value, type or None)` - `slots` - tuple of slot names - `css` - tuple of CSS file URLs - `js` - tuple of JS file URLs ```python 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 `//`: ```python 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 ```python 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 ```python 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: ```python # 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. ```python import os catalog = Catalog( "components/", auto_reload=os.environ.get("DEBUG", "false").lower() == "true", ) ``` Or based on your framework's debug setting: ```python # 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: ```html+jinja title="components/popover.jx" {#def label, content #} {% set popover_id = _get_random_id("popover") %}
{{ content }}
``` 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: ```html+jinja title="components/input.jx" {#def name, label="", id="" #} {% set input_id = id or _get_random_id(name) %} {% if label %} {% endif %} ``` --- title: Components description: Building components with Jx id: components url: /docs/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 `.jx` extension: ```html+jinja title="components/button.jx" {#def text #} ``` ### Anatomy of a Component A complete component can have these parts: ```html+jinja title="components/card.jx" {#import "./header.jx" as Header #} {#css card.css #} {#js card.js #} {#def title, subtitle="" #}
{{ content }}
``` From top to bottom: 1. **Imports** - Other components this one uses 2. **Assets** - CSS and JS files 3. **Arguments** - Data the component accepts 4. **Template** - The HTML to render All parts are optional except the template. ## Using Components Import a component, then use it like an HTML tag: ```html+jinja {#import "button.jx" as Button #} {#import "card.jx" as Card #}

Hello world!

{% endslot %} ``` **Fill slots** when using the component with `{% fill %}`: ```html+jinja title="usage" {% fill header %}

Confirm

{% endfill %}

Are you sure?

{% fill footer %} {% endfill %}
``` Unfilled slots use their default content. ### When to Use Slots vs Props #### Use Props When: - The content is a single value - You want validation ```html+jinja {#def title, count #}

{{ title }}: {{ count }}

``` #### Use Content When: - The content is HTML - There's one main content area - You want flexibility in what gets passed ```html+jinja {#def title #}

{{ title }}

{{ content }}
``` #### Use Named Slots When: - You need multiple content areas - Each area has a specific purpose - You might want to provide defaults for each area ```html+jinja
{% slot header %}Default{% endslot %}
{{ content }}
{% slot footer %}Default{% endslot %}
``` ## Validation Jx includes a CLI tool to validate all your components at once: ```bash โฏโฏ jx check myapp.setup:catalog ``` This catches import errors, unimported tags, typos, and more. See the [Validator](/docs/tools/check/) page for details. --- title: Attrs description: Handling extra HTML attributes id: attrs url: /docs/attrs/ --- The `attrs` object is one of Jx's most useful features. It collects any HTML attributes you pass to a component that aren't declared in its `{#def}` statement. ## The Problem Attrs Solves Imagine a button component: ```html+jinja {#def text #} ``` This works, but what if you need to add an `id`, `disabled`, or `data-*` attribute? Do you add them all to `{#def}`? That's impractical. Instead, use `attrs`: ```html+jinja {#def text #} ``` Now any extra attributes are automatically collected and rendered: ```html+jinja ``` ## How It Works When you pass attributes to a component: 1. **Declared arguments** (from `{#def}`) are extracted and available as variables 2. **Everything else** goes into the `attrs` object 3. You call `attrs.render()` to output them as HTML attributes ```html+jinja {#def title #} {# 'title' is a declared argument #}
{# Everything else becomes attrs #}

{{ title }}

{{ content }}
``` ```html+jinja ``` Here: - `title="Hello"` โ†’ Available as `{{ title }}` - `class="..."`, `id="..."`, `data-index="..."` โ†’ Go into `attrs` ## Basic Usage ### Rendering All Attrs The simplest use case: ```html+jinja
...
``` This outputs all extra attributes as HTML. ### Adding Default Attributes You can provide default attributes: ```html+jinja ``` If the user passes `class` or `type`, they'll be merged/overridden appropriately. ### Class Merging The `class` attribute is special; it merges instead of replacing: ```html+jinja {#def text #} ``` ```html+jinja ``` Both classes are included! ## Attrs Methods The `attrs` object has several useful methods: ### `attrs.render(**kwargs)` Renders all attributes as an HTML string. You can pass additional attributes to merge: ```html+jinja
...
``` **Merging rules:** - `class`: Classes are appended (not replaced) - Other attributes: New values override old values - `True`: Renders as a boolean attribute (e.g., `disabled`) - `False`: Removes the attribute - Underscores become dashes: `data_id` โ†’ `data-id` ### `attrs.set(**kwargs)` Modifies attributes before rendering: ```html+jinja {#def title, highlighted=false #} {% if highlighted %} {% do attrs.set(class="card card-highlighted", role="alert") %} {% endif %}

{{ title }}

{{ content }}
``` **Options:** - `attrs.set(id="new-id")` - Set an attribute - `attrs.set(disabled=True)` - Add a boolean attribute - `attrs.set(class="extra-class")` - Add to existing classes - `attrs.set(data_foo="bar")` - Underscores become dashes - `attrs.set(hidden=False)` - Remove an attribute ### `attrs.setdefault(**kwargs)` Sets an attribute only if it doesn't already exist: ```html+jinja {% do attrs.setdefault(role="button", tabindex=0) %}
{{ content }}
``` If the user passed `role`, it won't be overridden. ### `attrs.get(name, default=None)` Gets the value of an attribute: ```html+jinja {%- set btn_type = attrs.get("type", "button") %} ``` ### `attrs.add_class(*classes)` Adds one or more classes to the end of the class list: ```html+jinja {% do attrs.add_class("btn", "btn-primary") %} ``` ### `attrs.prepend_class(*classes)` Adds one or more classes to the beginning of the class list: ```html+jinja {% do attrs.prepend_class("btn") %} ``` This is useful when class order matters (e.g., with utility-first CSS frameworks where the first class should be the base style). ### `attrs.remove_class(*classes)` Removes one or more classes: ```html+jinja {% do attrs.remove_class("hidden", "invisible") %}
{{ content }}
``` ### `attrs.classes` Returns all the HTML classes as a space-separated string: ```html+jinja {% if "active" in attrs.classes %} This item is active {% endif %} ``` ### `attrs.as_dict` Returns all attributes as a dictionary: ```html+jinja {% set all_attrs = attrs.as_dict %} {% for key, value in all_attrs.items() %}

{{ key }}: {{ value }}

{% endfor %} ``` ## Common Patterns ### Button Component ```html+jinja title="components/button.jx" {#def text="Click me", variant="primary" #} ``` ```html+jinja title="usage" ``` This is especially useful for: - `data-*` attributes: `data_id`, `data_action` - `aria-*` attributes: `aria_label`, `aria_hidden` - Framework attributes: `hx_get`, `x_show`, `v_if` ## Special Cases ### Preserving Attributes Sometimes you want to control which attributes are rendered: ```html+jinja {#def title #} {# Get specific attrs before rendering #} {% set custom_id = attrs.get("id", "default-id") %}

{{ title }}

{# Other attrs go here #} {{ content }}
``` ### Conditional Attributes ```html+jinja {#def is_active=false #} {% if is_active %} {% do attrs.add_class("active") %} {% do attrs.set(aria_current="true") %} {% endif %} {{ content }} ``` ### Multiple Elements You can use attrs on multiple elements, but usually you want different attributes on each: ```html+jinja {#def title #}

{# Attrs go on the title #} {{ title }}

{{ content }}
``` Or split them: ```html+jinja {#def title #} {% set card_class = attrs.get("card_class", "card") %} {% do attrs.remove_class(card_class) %}

{# Other attrs go on title #} {{ title }}

{{ content }}
``` ## Best Practices ### 1. Always Provide Default Classes ```html+jinja {# โœ… Good - ensures base styling #} ``` ### 4. Don't Overuse attrs.set() ```html+jinja {# โŒ Too much manipulation #} {% do attrs.set(class="a") %} {% do attrs.add_class("b") %} {% do attrs.set(role="button") %} {% do attrs.setdefault(tabindex=0) %} {# โœ… Better - do it all at once #} {% do attrs.set(class="a b", role="button") %} {% do attrs.setdefault(tabindex=0) %} ``` ## Next Steps - **[Assets](/docs/assets)** - Learn about CSS and JavaScript management - **[API: Attrs](/docs/api/attrs)** - Full API reference for the Attrs class --- title: Assets description: Managing CSS and JavaScript in components id: assets url: /docs/assets/ --- Any component can declare the URLs of the CSS and JavaScript files it uses. Jx automatically collects these assets from all the components you use and provides simple functions to render them. ## Why Per-Component Assets? Traditional approaches put all CSS and JS in global files. This has problems: - **Hard to maintain**: Which styles belong to which component? - **Bloat**: Load everything even if you only use a few components - **Coupling**: Can't move/share components without hunting down their styles With per-component assets: - **Portability**: Copy a component folder, and its assets come with it - **Clarity**: Each component declares what it needs - **Performance**: Only load assets for components you actually use - **Testing**: Test component styles and behavior together ## Declaring Assets Use `{#css ... #}` and `{#js ... #}` comments at the top of your component: ```html+jinja title="components/card.jx" {#css card.css, animations.css #} {#js card.js #} {#def title #}

{{ title }}

{{ content }}
``` Multiple files are comma-separated. Each file can be: - **Relative**: `card.css` (relative to your static files) - **Absolute path**: `/static/styles/global.css` - **URL**: `https://cdn.example.com/library.js` ## The `assets` Global When you render a component, Jx provides an `assets` global object with methods to collect and render assets. ### `assets.render()` The simplest approach; renders both CSS and JS: ```html+jinja title="components/layout.jx" {#css layout.css #} {#js layout.js #} My App {{ assets.render() }} {{ content }} ``` This collects assets from the layout component and all components it imports, then renders them as `` and ` ``` **Options:** - `module=True` (default): Add `type="module"` - `module=False`: Regular scripts - `defer=True`: Add `defer` attribute (only when `module=False`) ```html+jinja {# ES modules (default) #} {{ assets.render_js() }} {# #} {# Regular deferred scripts #} {{ assets.render_js(module=False) }} {# #} {# Regular non-deferred scripts #} {{ assets.render_js(module=False, defer=False) }} {# #} ``` ## Collection Methods For more control, use the collection methods: ### `assets.collect_css()` Returns a list of all CSS file URLs: ```html+jinja {% for url in assets.collect_css() %} {% endfor %} ``` ### `assets.collect_js()` Returns a list of all JS file URLs: ```html+jinja {% for url in assets.collect_js() %} {% endfor %} ``` ## How Asset Collection Works Jx collects assets by walking the component tree: 1. Start with the root component you're rendering 2. Collect its CSS and JS declarations 3. Recursively collect from each imported component 4. Deduplicate (each file appears only once) 5. Return in dependency order **Example:** ```html+jinja title="page.jx" {#import "./layout.jx" as Layout #} {#import "./card.jx" as Card #} {#css page.css #} ... ``` ```html+jinja title="layout.jx" {#import "./header.jx" as Header #} {#css layout.css #}
{{ content }}
``` ```html+jinja title="header.jx" {#css header.css #}
...
``` ```html+jinja title="card.jx" {#css card.css #}
{{ content }}
``` **Collected CSS (in order):** ``` page.css layout.css header.css card.css ``` Each imported component's assets are collected recursively. ## Asset URLs Jx doesn't process or rewrite asset URLs; they're used exactly as you write them. ### Relative URLs ```html+jinja {#css card.css #} {#js card.js #} ``` **Output:** ```html ``` How these resolve depends on your HTML base path and server configuration. ## Organizing Assets ### Option 1: Keep assets next to components ``` components/ card/ card.jx card.css card.js button/ button.jx button.css button.js ``` ```html+jinja title="components/card/card.jx" {#css /static/components/card/card.css #} {#js /static/components/card/card.js #} ``` ### Option 2: Put component assets in a static folder Keep components and assets separate: ``` components/ card.jx button.jx static/ css/ card.css button.css js/ card.js button.js ``` Use absolute paths: ```html+jinja title="components/card.jx" {#css /static/css/card.css #} {#js /static/js/card.js #} ``` ```html+jinja title="components/layout.jx" {{ assets.render() }} ``` Or relative ones and use your web framework to resolve them: ```html+jinja title="components/card.jx" {#css css/card.css #} {#js js/card.js #} ``` ```html+jinja title="components/layout.jx" {% for name in assets.collect_css() %} {% endfor %} {% for name in assets.collect_js() %} {% endfor %} ``` ### Option 3: Build tool integration Use Vite, Webpack, or another bundler: ```html+jinja title="components/card.jx" {#css /dist/card.css #} {#js /dist/card.js #} ``` Your build tool will generate the files with hashes for cache-busting: ```html ``` ## Best Practices ### 1. Declare Third-Party Dependencies ```html+jinja {# โœ… Good - explicit dependencies #} {#css https://cdn.example.com/library.css #} {#import "./component-using-library.jx" as Component #} ``` ### 2. Keep Asset Files Small Each component should have focused styles and scripts: ```html+jinja {# โœ… Good - focused component #} {#css button.css #} {# ~2KB #} {#js button.js #} {# ~1KB #} {# โŒ Bad - too much stuff #} {#css button-and-everything-else.css #} {# ~50KB #} ``` ### 3. Use CSS Scoping ```css /* โœ… Good - scoped to component */ .Card { padding: 1rem; } .Card__title { font-size: 1.5rem; } /* โŒ Bad - will affect everything */ h3 { font-size: 1.5rem; } ``` Modern browsers support [CSS nesting](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_nesting): ```css .Card { padding: 1rem; & h3 { font-size: 1.5rem; } } ``` ## No Middleware Required Jx doesn't require middleware to serve component assets. You serve them however you want: - **Static files**: Configure your web framework to serve from `static/` - **CDN**: Upload to S3/CloudFront and reference those URLs - **Build tools**: Use Vite/Webpack to bundle and serve - **Reverse proxy**: Nginx/Caddy serve static files Jx just collects the URLs you declare and renders them as tags. ## Performance Considerations ### Asset Deduplication Jx automatically deduplicates assets. If multiple components declare the same CSS file, it's only included once: ```html+jinja {# card.jx uses common.css #} {# button.jx uses common.css #} {# page.jx uses both #} ``` Results in: ```html ``` ### Loading Order Assets are collected in dependency order: 1. Parent component assets first 2. Then imported component assets 3. In the order they're imported This ensures proper cascade and dependency resolution. --- title: Catalog class id: api-catalog url: /docs/api/catalog/ --- ## `class`{ .autodoc-symbol .autodoc-symbol-class } `Catalog`{ .autodoc-name .autodoc-name-class } :::: div autodoc-short-description Manager of the components and their global settings. :::: ````python Catalog( folder: str | pathlib._local.Path | None = None, *, jinja_env: jinja2.environment.Environment | None = None, extensions: list | None = None, filters: dict[str, typing.Any] | None = None, tests: dict[str, typing.Any] | None = None, auto_reload: bool = True, asset_resolver: collections.abc.Callable[[str, str], str] | None = None, file_ext: str = '.jx', **template_globals: Any ) -> None ```` :::: div autodoc-table autodoc-arguments Argument | Description -------- | -------- `folder` | Optional folder path to scan for components. It's a shortcut to
calling `add_folder` when only one is used. `jinja_env` | Optional Jinja2 environment to use for rendering. `extensions` | Optional extra Jinja2 extensions to add to the environment. `filters` | Optional extra Jinja2 filters to add to the environment. `tests` | Optional extra Jinja2 tests to add to the environment. `auto_reload` | Whether to check the last-modified time of the components files and
automatically re-process them if they change. The performance impact of
leaving it on is minimal, but *might* be noticeable when rendering a
component that uses a large number of child components. `asset_resolver` | Optional callable that transforms asset URLs for components from
folders registered with an `assets` folder.
Receives `(url, prefix)` and returns the resolved URL.
Only invoked for components whose prefix has a registered assets
folder; all other asset URLs pass through unchanged. `file_ext` | File extension (including the leading dot) used to discover
component files within registered folders. Defaults to `.jx`.
Set to `.jinja` to keep the legacy naming, or any other value
if you prefer your own convention. `**template_globals` | Variables to make available to all components by default. :::: ::::: div autodoc-methods ### `function`{ .autodoc-symbol .autodoc-symbol-function } `add_folder`{ .autodoc-name .autodoc-name-function } :::: div autodoc-short-description Add a folder path from which to search for components, optionally under a prefix. :::: ````python add_folder( path: str | pathlib._local.Path, *, prefix: str = '', assets: str | pathlib._local.Path | None = None ) -> None ```` :::: div autodoc-table autodoc-arguments Argument | Description -------- | -------- `path` | Absolute path of the folder with component files. `prefix` | Optional path prefix that all the components in the folder
will have. The default is empty. `assets` | Optional path to a folder containing CSS/JS assets for
this folder's components. When set, the `asset_resolver`
will be invoked for asset URLs from these components. :::: :::: div autodoc-long-description Components without a prefix can be imported as a path relative to the folder, e.g.: `sub/folder/component.jx` or with a path relative to the component where it is used: `./folder/component.jx`. Relative imports cannot go outside the folder. Components added with a prefix must be imported using the `@prefix/` syntax: `@prefix/sub/folder/component.jx`. If the importing is done from within a component with the prefix itself, a relative import can also be used, e.g.: `./component.jx`. All the folders added under the same prefix will be treated as if they were a single folder. This means if you add two folders, under the same prefix, with a component with the same subpath/filename, the one in the folder added **first** will be used and the other ignored. WARNING: You cannot move or delete components files from the folder after calling this method, but you can call it again to add new components added to the folder. :::: ### `function`{ .autodoc-symbol .autodoc-symbol-function } `add_package`{ .autodoc-name .autodoc-name-function } :::: div autodoc-short-description Register components (and optionally assets) from an installed Python package. :::: ````python add_package( package_name: str, *, prefix: str ) -> None ```` :::: div autodoc-table autodoc-arguments Argument | Description -------- | -------- `package_name` | The importable package name (e.g. `"my_ui_kit"`). `prefix` | Prefix for the components (e.g. `"ui"`). :::: :::: div autodoc-long-description The package module must expose a `JX_COMPONENTS` attribute pointing to the components folder (e.g. via `importlib.resources.files`). It may also expose `JX_ASSETS` pointing to an assets folder. :::: ### `function`{ .autodoc-symbol .autodoc-symbol-function } `add_folder`{ .autodoc-name .autodoc-name-function } :::: div autodoc-short-description Add a folder path from which to search for components, optionally under a prefix. :::: ````python add_folder( path: str | pathlib._local.Path, *, prefix: str = '', assets: str | pathlib._local.Path | None = None ) -> None ```` :::: div autodoc-table autodoc-arguments Argument | Description -------- | -------- `path` | Absolute path of the folder with component files. `prefix` | Optional path prefix that all the components in the folder
will have. The default is empty. `assets` | Optional path to a folder containing CSS/JS assets for
this folder's components. When set, the `asset_resolver`
will be invoked for asset URLs from these components. :::: :::: div autodoc-long-description Components without a prefix can be imported as a path relative to the folder, e.g.: `sub/folder/component.jx` or with a path relative to the component where it is used: `./folder/component.jx`. Relative imports cannot go outside the folder. Components added with a prefix must be imported using the `@prefix/` syntax: `@prefix/sub/folder/component.jx`. If the importing is done from within a component with the prefix itself, a relative import can also be used, e.g.: `./component.jx`. All the folders added under the same prefix will be treated as if they were a single folder. This means if you add two folders, under the same prefix, with a component with the same subpath/filename, the one in the folder added **first** will be used and the other ignored. WARNING: You cannot move or delete components files from the folder after calling this method, but you can call it again to add new components added to the folder. :::: ### `function`{ .autodoc-symbol .autodoc-symbol-function } `collect_assets`{ .autodoc-name .autodoc-name-function } :::: div autodoc-short-description Copy all registered package assets to an output folder. :::: ````python collect_assets( output: str | pathlib._local.Path ) -> list[tuple[str, pathlib._local.Path]] ```` :::: div autodoc-table autodoc-arguments Argument | Description -------- | -------- `output` | Destination folder. :::: :::: div autodoc-long-description For each prefix that has a registered assets folder, files are copied to `//`. Returns a list of `(prefix, relative_path)` tuples for every file copied. :::: ### `function`{ .autodoc-symbol .autodoc-symbol-function } `get_assets_folder`{ .autodoc-name .autodoc-name-function } :::: div autodoc-short-description Return the registered assets folder for a given prefix, or `None`. :::: ````python get_assets_folder( prefix: str ) -> pathlib._local.Path | None ```` :::: div autodoc-table autodoc-arguments Argument | Description -------- | -------- `prefix` | The prefix to look up (e.g. `"ui"`). :::: ### `function`{ .autodoc-symbol .autodoc-symbol-function } `get_component`{ .autodoc-name .autodoc-name-function } :::: div autodoc-short-description Instantiate and return a component object by its relative path. :::: ````python get_component( relpath: str ) -> jx.component.Component ```` :::: div autodoc-table autodoc-arguments Argument | Description -------- | -------- `relpath` | The path of the component to render, including the extension, relative to its view folder.
e.g.: "sub/component.jx". Always use the forward slash (/) as the path separator. :::: ### `function`{ .autodoc-symbol .autodoc-symbol-function } `get_component_data`{ .autodoc-name .autodoc-name-function } :::: div autodoc-short-description Get the component data from the cache. If the file has been updated, the component is re-processed. :::: ````python get_component_data( relpath: str ) -> jx.catalog.CData ```` :::: div autodoc-table autodoc-arguments Argument | Description -------- | -------- `relpath` | The path of the component to render, including the extension, relative to its view folder.
e.g.: "sub/component.jx". Always use the forward slash (/) as the path separator. :::: ### `function`{ .autodoc-symbol .autodoc-symbol-function } `get_signature`{ .autodoc-name .autodoc-name-function } :::: div autodoc-short-description Return a component's signature including its arguments and metadata. :::: ````python get_signature( relpath: str ) -> dict[str, typing.Any] ```` :::: div autodoc-table autodoc-arguments Argument | Description -------- | -------- `relpath` | The path of the component, including the extension, relative to its view folder.
e.g.: "sub/component.jx". Always use the forward slash (/) as the path separator. :::: :::: div autodoc-returns **Returns:** A dictionary containing: - required: dict of required argument names mapped to their type (or None) - optional: dict of optional arguments mapped to (default_value, type or None) - slots: tuple of slot names - css: tuple of CSS file URLs - js: tuple of JS file URLs :::: ### `function`{ .autodoc-symbol .autodoc-symbol-function } `has`{ .autodoc-name .autodoc-name-function } :::: div autodoc-short-description Return True if a component with the given relative path is registered. :::: ````python has( relpath: str ) -> bool ```` :::: div autodoc-long-description Does not read the file or recompile โ€” just checks the catalog index. :::: ### `function`{ .autodoc-symbol .autodoc-symbol-function } `list_components`{ .autodoc-name .autodoc-name-function } :::: div autodoc-short-description Return all registered component paths. :::: ````python list_components() -> list[str] ```` :::: div autodoc-returns **Returns:** A list of component relative paths (e.g., ["button.jx", "card.jx"]). :::: ### `function`{ .autodoc-symbol .autodoc-symbol-function } `render`{ .autodoc-name .autodoc-name-function } :::: div autodoc-short-description Render a component with the given relative path and context. :::: ````python render( relpath: str, globals: dict[str, typing.Any] | None = None, **kwargs ) -> str ```` :::: div autodoc-table autodoc-arguments Argument | Description -------- | -------- `relpath` | The path of the component to render, including the extension, relative to its view folder.
e.g.: "sub/component.jx". Always use the forward slash (/) as the path separator. `globals` | Optional global variables to make available to the component and all its
imported components. `**kwargs` | Keyword arguments to pass to the component.
They will be available in the component's context but not to its imported components. :::: :::: div autodoc-returns **Returns:** The rendered component as a string. :::: ### `function`{ .autodoc-symbol .autodoc-symbol-function } `render_string`{ .autodoc-name .autodoc-name-function } :::: div autodoc-short-description Render a component from a string source. Works like `render`, but the component is not cached and cannot do relative imports. :::: ````python render_string( source: str, globals: dict[str, typing.Any] | None = None, **kwargs ) -> str ```` :::: div autodoc-table autodoc-arguments Argument | Description -------- | -------- `source` | The Jinja2 source code of the component to render. `globals` | Optional global variables to make available to the component and all its
imported components. `**kwargs` | Keyword arguments to pass to the component.
They will be available in the component's context but not to its imported components. :::: :::: div autodoc-returns **Returns:** The rendered component as a string. :::: ::::: --- title: jx.Attrs class id: api-attrs url: /docs/api/attrs/ --- ## `class`{ .autodoc-symbol .autodoc-symbol-class } `Attrs`{ .autodoc-name .autodoc-name-class } :::: div autodoc-short-description Contains all the HTML attributes/properties (a property is an attribute without a value) passed to a component but that weren't in the declared attributes list. :::: ````python Attrs( attrs: 'dict[str, t.Any | LazyString]' ) -> None ```` :::: div autodoc-long-description For HTML classes you can use the name "classes" (instead of "class") if you need to. **NOTE**: The string values passed to this class, are not cast to `str` until the string representation is actually needed, for example when `attrs.render()` is invoked. :::: ::::: div autodoc-properties ### `attr`{ .autodoc-symbol .autodoc-symbol-attr } `as_dict`{ .autodoc-name .autodoc-name-attr } `property`{ .autodoc-label .autodoc-label-property } :::: div autodoc-short-description An ordered dict of all the attributes and properties, both sorted by name before join. :::: :::: div autodoc-examples **Example:** ::: div ```python attrs = Attrs({ "class": "lorem ipsum", "data_test": True, "hidden": True, "aria_label": "hello", "id": "world", }) attrs.as_dict { "aria_label": "hello", "class": "lorem ipsum", "id": "world", "data_test": True, "hidden": True } ``` ::: :::: ### `attr`{ .autodoc-symbol .autodoc-symbol-attr } `classes`{ .autodoc-name .autodoc-name-attr } `property`{ .autodoc-label .autodoc-label-property } :::: div autodoc-short-description All the HTML classes separated by a space. :::: :::: div autodoc-examples **Example:** ::: div ```python attrs = Attrs({"class": "italic bold bg-blue wide abcde"}) attrs.set(class="bold text-white") print(attrs.classes) italic bold bg-blue wide abcde text-white ``` ::: :::: ::::: ::::: div autodoc-methods ### `function`{ .autodoc-symbol .autodoc-symbol-function } `add_class`{ .autodoc-name .autodoc-name-function } :::: div autodoc-short-description Adds one or more classes to the end of the list of classes, if not already present. :::: ````python add_class( *values: str ) -> None ```` :::: div autodoc-table autodoc-arguments Argument | Description -------- | -------- `values` | One or more class names to add, separated by spaces. :::: :::: div autodoc-examples **Example:** ::: div ```python attrs = Attrs({"class": "a b c"}) attrs.add_class("c d") attrs.as_dict {"class": "a b c d"} ``` ::: :::: ### `function`{ .autodoc-symbol .autodoc-symbol-function } `get`{ .autodoc-name .autodoc-name-function } :::: div autodoc-short-description Returns the value of the attribute or property, or the default value if it doesn't exist. :::: ````python get( name: str, default: Any = None ) -> Any ```` :::: div autodoc-table autodoc-arguments Argument | Description -------- | -------- `name` | The name of the attribute or property to get. `default` | The default value to return if the attribute or property doesn't exist. :::: :::: div autodoc-examples **Example:** ::: div ```python attrs = Attrs({"lorem": "ipsum", "hidden": True}) attrs.get("lorem", default="bar") 'ipsum' attrs.get("foo") None attrs.get("foo", default="bar") 'bar' attrs.get("hidden") True ``` ::: :::: ### `function`{ .autodoc-symbol .autodoc-symbol-function } `prepend_class`{ .autodoc-name .autodoc-name-function } :::: div autodoc-short-description Adds one or more classes to the beginning of the list of classes, if not already present. :::: ````python prepend_class( *values: str ) -> None ```` :::: div autodoc-table autodoc-arguments Argument | Description -------- | -------- `values` | One or more class names to add, separated by spaces. :::: :::: div autodoc-examples **Example:** ::: div ```python attrs = Attrs({"class": "a b c"}) attrs.prepend_class("c d |") attrs.as_dict {"class": "d | a b c"} ``` ::: :::: ### `function`{ .autodoc-symbol .autodoc-symbol-function } `remove_class`{ .autodoc-name .autodoc-name-function } :::: div autodoc-short-description Removes one or more classes from the list of classes. :::: ````python remove_class( *names: str ) -> None ```` :::: div autodoc-examples **Example:** ::: div ```python attrs = Attrs({"class": "a b c"}) attrs.remove_class("c", "d") attrs.as_dict {"class": "a b"} ``` ::: :::: ### `function`{ .autodoc-symbol .autodoc-symbol-function } `render`{ .autodoc-name .autodoc-name-function } :::: div autodoc-short-description Renders the attributes and properties as a string. :::: ````python render( **kw ) -> str ```` :::: div autodoc-long-description Any arguments you use with this function are merged with the existing attributes/properties by the same rules as the `Attrs.set()` function: - Pass a name and a value to set an attribute (e.g. `type="text"`) - Use `True` as a value to set a property (e.g. `disabled`) - Use `False` to remove an attribute or property - If the attribute is "class", the new classes are appended to the old ones (if not repeated) instead of replacing them. - The underscores in the names will be translated automatically to dashes, so `aria_selected` becomes the attribute `aria-selected`. To provide consistent output, the attributes and properties are sorted by name and rendered like this: ` + `. :::: :::: div autodoc-examples **Example:** ::: div ```python attrs = Attrs({"class": "ipsum", "data_good": True, "width": 42}) attrs.render() 'class="ipsum" width="42" data-good' attrs.render(class="abc", data_good=False, tabindex=0) 'class="abc ipsum" width="42" tabindex="0"' # render classes come first ``` ::: :::: ### `function`{ .autodoc-symbol .autodoc-symbol-function } `set`{ .autodoc-name .autodoc-name-function } :::: div autodoc-short-description Sets an attribute or property :::: ````python set( **kw ) -> None ```` :::: div autodoc-long-description - Pass a name and a value to set an attribute (e.g. `type="text"`) - Use `True` as a value to set a property (e.g. `disabled`) - Use `False` to remove an attribute or property - If the attribute is "class", the new classes are appended to the old ones (if not repeated) instead of replacing them. - The underscores in the names will be translated automatically to dashes, so `aria_selected` becomes the attribute `aria-selected`. :::: :::: div autodoc-examples **Example:** ::: div ```python attrs = Attrs({"secret": "qwertyuiop"}) attrs.set(secret=False) attrs.as_dict {} attrs.set(unknown=False, lorem="ipsum", count=42, data_good=True) attrs.as_dict {"count":42, "lorem":"ipsum", "data_good": True} attrs = Attrs({"class": "b c a"}) attrs.set(class="c b f d e") attrs.as_dict {"class": "b c a f d e"} ``` ::: :::: ### `function`{ .autodoc-symbol .autodoc-symbol-function } `setdefault`{ .autodoc-name .autodoc-name-function } :::: div autodoc-short-description Adds an attribute, but only if it's not already present. :::: ````python setdefault( **kw ) -> None ```` :::: div autodoc-long-description The underscores in the names will be translated automatically to dashes, so `aria_selected` becomes the attribute `aria-selected`. :::: :::: div autodoc-examples **Example:** ::: div ```python attrs = Attrs({"lorem": "ipsum"}) attrs.setdefault(tabindex=0, lorem="meh") attrs.as_dict # "tabindex" changed but "lorem" didn't {"lorem": "ipsum", tabindex: 0} ``` ::: :::: ::::: --- title: Layout Components description: Creating reusable page layouts id: recipes-layouts url: /docs/recipes/layouts/ --- Layouts are components that wrap entire pages, providing consistent structure like headers, footers, and navigation. ## Basic Layout ```html+jinja title="components/layout.jx" {#def title #} {#css layout.css #} {{ title }} {{ assets.render_css() }} {{ assets.render_js() }}
{{ content }}

© 2026 My Site

``` ```html+jinja title="components/pages/home.jx" {#import "../layout.jx" as Layout #}

Welcome!

This is the home page.

``` ## Layout with Slots Use named slots for customizable sections: ```html+jinja title="components/layout.jx" {#def title #} {{ title }} {% slot head %}{% endslot %} {{ assets.render_css() }}
{% slot header %} {% endslot %}
{{ content }}
{% slot footer %}

© 2026

{% endslot %}
{% slot scripts %}{% endslot %} {{ assets.render_js() }} ``` ```html+jinja title="usage" {#import "layout.jx" as Layout #} {% fill head %} {% endfill %} {% fill header %} {% endfill %}

Dashboard

Welcome back!

{% fill scripts %} {% endfill %}
``` ## Nested Layouts Create specialized layouts that extend a base: ```html+jinja title="components/layouts/base.jx" {#def title #} {#css base.css #} {{ title }} | My App {{ assets.render_css() }} {{ content }} {{ assets.render_js() }} ``` ```html+jinja title="components/layouts/app.jx" {#import "./base.jx" as Base #} {#import "../sidebar.jx" as Sidebar #} {#def title #} {#css app.css #}
{{ content }}
``` ```html+jinja title="components/layouts/auth.jx" {#import "./base.jx" as Base #} {#def title #} {#css auth.css #}
{{ content }}
``` ```html+jinja title="usage" {#import "layouts/app.jx" as App #}

Dashboard

``` ```html+jinja title="usage" {#import "layouts/auth.jx" as Auth #}

Sign In

...
``` ## Layout with Navigation Highlighting Pass the current page to highlight active nav items: ```html+jinja title="components/layout.jx" {#def title, current_page="" #} {{ title }} {{ assets.render_css() }}
{{ content }}
{{ assets.render_js() }} ``` ```html+jinja title="pages/about.jx" {#import "layout.jx" as Layout #}

About Us

``` ## Conditional Layout Sections ```html+jinja title="components/layout.jx" {#def title, show_sidebar=true, show_footer=true #} {{ title }} {{ assets.render_css() }}
{% if show_sidebar %} {% endif %}
{{ content }}
{% if show_footer %}
{% slot footer %}

© 2026

{% endslot %}
{% endif %} {{ assets.render_js() }} ``` ```html+jinja title="usage" {# Full layout with sidebar #} {% fill sidebar %} {% endfill %}

Dashboard

{# Minimal layout without sidebar #}
Login form
``` --- title: SVG Icons description: Using components for SVG icons id: recipes-icons url: /docs/recipes/icons/ --- Components are perfect for SVG icons - encapsulate the SVG code once, reuse it everywhere with customizable size and color. ## Basic Icon Component ```html+jinja title="components/icons/icon-check.jx" {#def size=24 #} ``` ```html+jinja title="usage" {#import "icons/icon-check.jx" as IconCheck #} ``` ::: tab | Preview

::: ## Generic Icon Wrapper Create a base component that other icons extend: ```html+jinja title="components/icons/icon.jx" {#def size=24 #} {{ content }} ``` ```html+jinja title="components/icons/icon-x.jx" {#import "./icon.jx" as Icon #} {#def size=24 #} {% do attrs.set(size=size) %} ``` ```html+jinja title="components/icons/icon-menu.jx" {#import "./icon.jx" as Icon #} {#def size=24 #} {% do attrs.set(size=size) %} ``` ::: tab | Preview

::: ## Dynamic Icon Component Load icons by name: ```html+jinja title="components/icon.jx" {#def name, size=24 #} {% set icons = { "check": '', "x": '', "menu": '', "search": '', "user": '', } %} {{ icons.get(name, "") | safe }} ``` ```html+jinja title="usage" {#import "icon.jx" as Icon #} ``` ::: tab | Preview

::: ## Icon Button Combine icons with buttons: ```html+jinja title="components/icon-button.jx" {#def label="" #} {#css icon-button.css #} {% do attrs.setdefault(type="button") %} {% do attrs.set(aria_label=label if label else None) %} ``` ```html+jinja title="usage" {#import "icon-button.jx" as IconButton #} {#import "icons/icon-x.jx" as IconX #} ``` ::: tab | Preview

::: ## Button with Icon and Text ```html+jinja title="components/button.jx" {#def text="" #} {#css button.css #} {% do attrs.setdefault(type="button") %} ``` ```html+jinja title="usage" {#import "button.jx" as Button #} {#import "icons/icon-check.jx" as IconCheck #} ``` ::: tab | Preview

::: ## Filled vs Stroke Icons ```html+jinja title="components/icons/icon-heart.jx" {#def size=24, filled=false #} ``` ```html+jinja title="usage" {# Outline #} {# Filled #} ``` ::: tab | Preview

::: ## Spinner Icon ```html+jinja title="components/icons/icon-spinner.jx" {#def size=24 #} {#css spinner.css #} ``` ```css title="spinner.css" .icon-spinner { animation: spin 1s linear infinite; } @keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } ``` ::: tab | Preview

::: ## Icon with Badge ```html+jinja title="components/icon-badge.jx" {#def count=0 #} {#css icon-badge.css #} {{ content }} {% if count > 0 %} {{ count if count < 100 else "99+" }} {% endif %} ``` ```css title="icon-badge.css" .icon-badge-wrapper { position: relative; display: inline-flex; } .icon-badge { position: absolute; top: -10px; right: -10px; background: rgba(255,0,0,0.8); color: white; font-size: 10px; font-weight: bold; padding: 2px 6px; border-radius: 10px; } ``` ```html+jinja title="usage" {#import "icon-badge.jx" as IconBadge #} {#import "icons/icon-bell.jx" as IconBell #} ``` ::: tab | Preview

42

::: ## Tips 1. **Use `currentColor`** for fill/stroke to inherit text color 2. **Set sensible defaults** for size (24px is common) 3. **Add `aria-hidden="true"`** for decorative icons 4. **Use `aria-label`** on icon-only buttons 5. **Keep SVGs optimized** - remove unnecessary attributes --- title: Working with Flask description: Integrating Jx components with Flask applications id: working-flask url: /docs/working/flask/ --- [Flask](https://flask.palletsprojects.com/){target=_blank} is a lightweight Python web framework. Jx integrates seamlessly with Flask, giving you component-based templates while keeping access to Flask's utilities like `url_for`, `flash`, and session management. ## Basic Setup Create a catalog and use it in your views: ```python title="app.py" from flask import Flask from jx import Catalog app = Flask(__name__) catalog = Catalog( "components/", auto_reload=app.debug, ) @app.route("/") def home(): return catalog.render("pages/home.jx") ``` ## Using Flask's Jinja Environment Flask comes with its own Jinja environment that includes useful globals like `url_for`, `g`, `request`, `session`, and `config`. To access these in your components, share Flask's environment with Jx: ```python title="app.py" from flask import Flask from jx import Catalog app = Flask(__name__) # Share Flask's Jinja environment with Jx catalog = Catalog( "components/", jinja_env=app.jinja_env, auto_reload=app.debug, ) ``` Now your components have access to all Flask template utilities: ```html+jinja title="components/nav.jx" ``` This is also true for any Flask extension that adds globals to the templates. ## Adding Flask Globals Manually If you prefer not to share the entire Jinja environment, pass specific Flask utilities as globals: ```python title="app.py" from flask import Flask, url_for, request, g, session from jx import Catalog app = Flask(__name__) catalog = Catalog( "components/", auto_reload=app.debug, url_for=url_for, ) ``` For request-specific values, pass them when rendering: ```python title="views.py" from flask import request, session, g @app.route("/dashboard") def dashboard(): return catalog.render( "pages/dashboard.jx", globals={ "request": request, "session": session, "g": g, }, user=g.user, ) ``` ## Flash Messages Create a component to display Flask flash messages: ```html+jinja title="components/flash-messages.jx" {#css flash-messages.css #} {% with messages = get_flashed_messages(with_categories=true) %} {% if messages %}
{% for category, message in messages %}
{{ message }}
{% endfor %}
{% endif %} {% endwith %} ``` ```html+jinja title="components/layout.jx" {#import "./flash-messages.jx" as FlashMessages #} {#def title #} {{ title }} {{ assets.render_css() }}
{{ content }}
{{ assets.render_js() }} ``` ```python title="views.py" from flask import flash, redirect, url_for @app.route("/save", methods=["POST"]) def save(): # ... save logic ... flash("Changes saved successfully!", "success") return redirect(url_for("dashboard")) ``` ## CSRF Protection ### With Flask-WTF If you're using Flask-WTF for CSRF protection, create a component for the token: ```html+jinja title="components/csrf-input.jx" ``` Use it in forms: ```html+jinja title="components/login-form.jx" {#import "./csrf-input.jx" as CsrfInput #} {#def action #}
{{ content }} ``` ```html+jinja title="usage" {#import "login-form.jx" as Form #} {#import "input.jx" as Input #}
``` ## Blueprints Jx works well with Flask blueprints. You can use a single shared catalog or create separate catalogs per blueprint: ### Shared Catalog ```python title="app.py" from flask import Flask from jx import Catalog app = Flask(__name__) catalog = Catalog("components/", jinja_env=app.jinja_env, auto_reload=app.debug) # Make catalog available to blueprints app.catalog = catalog ``` ```python title="blueprints/blog.py" from flask import Blueprint, current_app blog = Blueprint("blog", __name__, url_prefix="/blog") @blog.route("/") def index(): posts = get_posts() return current_app.catalog.render("blog/index.jx", posts=posts) @blog.route("/") def post(slug): post = get_post_by_slug(slug) return current_app.catalog.render("blog/post.jx", post=post) ``` ### Blueprint-Specific Components Add component folders with prefixes for each blueprint: ```python title="app.py" from flask import Flask from jx import Catalog app = Flask(__name__) catalog = Catalog(jinja_env=app.jinja_env, auto_reload=app.debug) # Shared components catalog.add_folder("components/") # Blueprint-specific components catalog.add_folder("blueprints/blog/components/", prefix="blog") catalog.add_folder("blueprints/admin/components/", prefix="admin") app.catalog = catalog ``` ```html+jinja title="blueprints/blog/components/post-card.jx" {#import "card.jx" as Card #} {#def post #}

{{ post.title }}

{{ post.excerpt }}

``` ```html+jinja title="usage in blog templates" {#import "@blog/post-card.jx" as PostCard #} {% for post in posts %} {% endfor %} ``` ## Context Processors Use Flask's context processors to make variables available to all components: ```python title="app.py" @app.context_processor def inject_globals(): return { "site_name": "My App", "current_year": 2026, "is_authenticated": lambda: session.get("user_id") is not None, } ``` These are automatically available when using Flask's Jinja environment: ```html+jinja title="components/footer.jx"

© {{ current_year }} {{ site_name }}

``` ## Static Files Use Flask's `url_for` to reference static files: ```html+jinja title="components/layout.jx" {#def title #} {{ title }} {{ assets.render_css() }} {{ content }} {{ assets.render_js() }} ``` For component assets, you can use absolute paths that map to your static folder: ```html+jinja title="components/card.jx" {#css /static/css/card.css #} {#def title #}

{{ title }}

{{ content }}
``` Or use `url_for` in a custom render loop: ```html+jinja title="components/layout.jx" {% for css_file in assets.collect_css() %} {% endfor %} ``` ## Complete Example Here's a complete Flask application using Jx: ```python title="app.py" from flask import Flask, redirect, url_for, flash, session, g, request from flask_wtf.csrf import CSRFProtect from jx import Catalog app = Flask(__name__) app.secret_key = "your-secret-key" csrf = CSRFProtect(app) # Create catalog with Flask's Jinja environment catalog = Catalog( "components/", jinja_env=app.jinja_env, auto_reload=app.debug, ) @app.before_request def load_user(): user_id = session.get("user_id") g.user = get_user_by_id(user_id) if user_id else None @app.route("/") def home(): return catalog.render("pages/home.jx") @app.route("/login", methods=["GET", "POST"]) def login(): if request.method == "POST": user = authenticate(request.form["email"], request.form["password"]) if user: session["user_id"] = user.id flash("Welcome back!", "success") return redirect(url_for("dashboard")) flash("Invalid credentials", "error") return catalog.render("pages/login.jx") @app.route("/dashboard") def dashboard(): if not g.user: return redirect(url_for("login")) return catalog.render("pages/dashboard.jx", user=g.user) @app.errorhandler(404) def not_found(e): return catalog.render("errors/404.jx"), 404 if __name__ == "__main__": app.run(debug=True) ``` ```html+jinja title="components/layout.jx" {#import "./nav.jx" as Nav #} {#import "./flash-messages.jx" as FlashMessages #} {#import "./footer.jx" as Footer #} {#css layout.css #} {#def title #} {{ title }} | My App {{ assets.render_css() }}