Working with Flask

Flask 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:

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:

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:

components/nav.jx
<nav>
  <a href="{{ url_for('home') }}">Home</a>
  <a href="{{ url_for('about') }}">About</a>
  {% if session.get('user_id') %}
    <a href="{{ url_for('profile') }}">Profile</a>
    <a href="{{ url_for('logout') }}">Logout</a>
  {% else %}
    <a href="{{ url_for('login') }}">Login</a>
  {% endif %}
</nav>

This is also true for any Flask extension that adds globals to the templates. Context processors are a separate matter; see Context Processors below.

Adding Flask Globals Manually

If you prefer not to share the entire Jinja environment, pass specific Flask utilities as globals:

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:

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:

components/flash-messages.jx
{#css flash-messages.css #}

{% with messages = get_flashed_messages(with_categories=true) %}
  {% if messages %}
    <div class="flash-messages">
      {% for category, message in messages %}
        <div class="flash flash-{{ category }}">
          {{ message }}
          <button type="button" class="flash-close" onclick="this.parentElement.remove()">&times;</button>
        </div>
      {% endfor %}
    </div>
  {% endif %}
{% endwith %}
components/layout.jx
{#import "./flash-messages.jx" as FlashMessages #}
{#def title #}

<!DOCTYPE html>
<html>
<head>
  <title>{{ title }}</title>
  {{ assets.render_css() }}
</head>
<body>
  <FlashMessages />
  <main>
    {{ content }}
  </main>
  {{ assets.render_js() }}
</body>
</html>
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:

components/csrf-input.jx
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">

Use it in forms:

components/login-form.jx
{#import "./csrf-input.jx" as CsrfInput #}
{#def action #}

<form method="post" action="{{ action }}" {{ attrs.render() }}>
  <CsrfInput />
  {{ content }}
</form>
usage
{#import "login-form.jx" as Form #}
{#import "input.jx" as Input #}

<Form action="{{ url_for('login') }}">
  <Input name="email" type="email" label="Email" required />
  <Input name="password" type="password" label="Password" required />
  <button type="submit">Login</button>
</Form>

Blueprints

Jx works well with Flask blueprints. You can use a single shared catalog or create separate catalogs per blueprint:

Shared Catalog

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
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("/<slug>")
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:

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
blueprints/blog/components/post-card.jx
{#import "card.jx" as Card #}
{#def post #}

<Card class="post-card">
  <h2><a href="{{ url_for('blog.post', slug=post.slug) }}">{{ post.title }}</a></h2>
  <p>{{ post.excerpt }}</p>
</Card>
usage in blog templates
{#import "@blog/post-card.jx" as PostCard #}

{% for post in posts %}
  <PostCard post={{ post }} />
{% endfor %}

Context Processors

Flask runs @app.context_processor functions from inside render_template(). catalog.render() does not go through it, so sharing the Jinja environment is not enough to get them:

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,
    }
components/footer.jx
<footer>
  <p>&copy; {{ current_year }} {{ site_name }}</p>
</footer>

Rendered with catalog.render("footer.jx"), site_name comes out empty and is_authenticated() raises UndefinedError. Plain values fail silently; only calling one gives you an error.

Ask Flask for the context yourself and pass it as globals:

context = {}
app.update_template_context(context)
return catalog.render("pages/home.jx", globals=context)

A small helper keeps that out of every view:

app.py
def render(template, **values):
    context = dict(values)
    app.update_template_context(context)
    return catalog.render(template, globals=context)


@app.route("/")
def home():
    return render("pages/home.jx", user=g.user)

If the values do not change per request, skip context processors and hand them to the catalog once:

app.py
catalog = Catalog(
    "components/",
    jinja_env=app.jinja_env,
    site_name="My App",
    current_year=2026,
)

What works without any of this is whatever Flask puts in jinja_env.globals: url_for, get_flashed_messages, config, request, session and g. That is why the examples above use them directly. The same split applies to Flask extensions: their globals reach your components, their context processors do not.

For the same reason, the before_render_template and template_rendered signals do not fire for catalog.render(). Tools that rely on them, such as Flask-DebugToolbar or captured_templates in tests, will not see components rendered this way.

Static Files

Use Flask's url_for to reference static files:

components/layout.jx
{#def title #}

<!DOCTYPE html>
<html>
<head>
  <title>{{ title }}</title>
  <link rel="icon" href="{{ url_for('static', filename='favicon.ico') }}">
  {{ assets.render_css() }}
</head>
<body>
  {{ content }}
  {{ assets.render_js() }}
</body>
</html>

For component assets, you can use absolute paths that map to your static folder:

components/card.jx
{#css /static/css/card.css #}
{#def title #}

<div {{ attrs.render(class="card") }}>
  <h3>{{ title }}</h3>
  {{ content }}
</div>

Or use url_for in a custom render loop:

components/layout.jx
<head>
  {% for css_file in assets.collect_css() %}
    <link rel="stylesheet" href="{{ url_for('static', filename=css_file) }}">
  {% endfor %}
</head>

Complete Example

Here's a complete Flask application using Jx:

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)
components/layout.jx
{#import "./nav.jx" as Nav #}
{#import "./flash-messages.jx" as FlashMessages #}
{#import "./footer.jx" as Footer #}
{#css layout.css #}
{#def title #}

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>{{ title }} | My App</title>
  {{ assets.render_css() }}
</head>
<body>
  <Nav />
  <FlashMessages />
  <main>
    {{ content }}
  </main>
  <Footer />
  {{ assets.render_js() }}
</body>
</html>
components/pages/dashboard.jx
{#import "../layout.jx" as Layout #}
{#import "../card.jx" as Card #}
{#def user #}

<Layout title="Dashboard">
  <h1>Welcome, {{ user.name }}!</h1>

  <div class="dashboard-grid">
    <Card title="Profile">
      <p>{{ user.email }}</p>
      <a href="{{ url_for('profile') }}">Edit Profile</a>
    </Card>

    <Card title="Settings">
      <a href="{{ url_for('settings') }}">Manage Settings</a>
    </Card>
  </div>
</Layout>

Production Settings

For production, disable auto-reload:

app.py
import os

app = Flask(__name__)
is_production = os.environ.get("FLASK_ENV") == "production"

catalog = Catalog(
    "components/", 
    jinja_env=app.jinja_env,
    auto_reload=not is_production,
)