Assets

Any component can declare the URLs of the CSS and JavaScript files that 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:

components/card.jinja
{#css card.css, animations.css #}
{#js card.js #}
{#def title #}

<div class="card">
  <h3>{{ title }}</h3>
  <div class="card-body">
    {{ content }}
  </div>
</div>

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:

components/layout.jinja
{#css layout.css #}
{#js layout.js #}

<!DOCTYPE html>
<html>
<head>
  <title>My App</title>
  {{ assets.render() }}
</head>
<body>
  {{ content }}
</body>
</html>

This collects assets from the layout component and all components it imports, then renders them as <link> and <script> tags.

assets.render_css()

Renders only CSS as <link> tags:

<head>
  <title>My App</title>
  {{ assets.render_css() }}
</head>

Output:

<link rel="stylesheet" href="layout.css">
<link rel="stylesheet" href="card.css">
<link rel="stylesheet" href="button.css">

assets.render_js(module=True, defer=True)

Renders JavaScript as <script> tags:

<body>
  {{ content }}
  {{ assets.render_js() }}
</body>

Output (default):

<script type="module" src="layout.js"></script>
<script type="module" src="card.js"></script>

Options:

  • module=True (default): Add type="module"
  • module=False: Regular scripts
  • defer=True: Add defer attribute (only when module=False)
{# ES modules (default) #}
{{ assets.render_js() }}
{# <script type="module" src="..."></script> #}

{# Regular deferred scripts #}
{{ assets.render_js(module=False) }}
{# <script src="..." defer></script> #}

{# Regular non-deferred scripts #}
{{ assets.render_js(module=False, defer=False) }}
{# <script src="..."></script> #}

Collection Methods

For more control, use the collection methods:

assets.collect_css()

Returns a list of all CSS file URLs:

{% for url in assets.collect_css() %}
  <link rel="stylesheet" href="{{ url }}">
{% endfor %}

assets.collect_js()

Returns a list of all JS file URLs:

{% for url in assets.collect_js() %}
  <script type="module" src="{{ url }}"></script>
{% 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:

page.jinja
{#import "./layout.jinja" as Layout #}
{#import "./card.jinja" as Card #}
{#css page.css #}

<Layout>
  <Card>...</Card>
</Layout>
layout.jinja
{#import "./header.jinja" as Header #}
{#css layout.css #}

<div>
  <Header />
  {{ content }}
</div>
header.jinja
{#css header.css #}
<header>...</header>
card.jinja
{#css card.css #}
<div class="card">{{ content }}</div>

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

{#css card.css #}
{#js card.js #}

Output:

<link rel="stylesheet" href="card.css">
<script type="module" src="card.js"></script>

How these resolve depends on your HTML base path and server configuration.

Organizing Assets

Option 1: Use

Keep assets next to components:

components/
  card/
    card.jinja
    card.css
    card.js
  button/
    button.jinja
    button.css
    button.js
components/card/card.jinja
{#css /static/components/card/card.css #}
{#js /static/components/card/card.js #}

Option 1: Put components assets in the static folder

Keep components and assets separate:

components/
  card.jinja
  button.jinja
static/
  css/
    card.css
    button.css
  js/
    card.js
    button.js

Use absolute paths

components/card.jinja
{#css /static/css/card.css #}
{#js /static/js/card.js #}
components/layout.jinja
{{ assets.render() }}

Or relative ones and use your web framework to resolve them:

components/card.jinja
{#css css/card.css #}
{#js js/card.js #}
components/layout.jinja
{% for name in assets.collect_css() %}
  <link rel="stylesheet" href="{{ url_for('static', filename=name) }}">
{% endfor %}
{% for name in assets.collect_js() %}
  <script type="module" src="{{ url_for('static', filename=name) }}"></script>
{% endfor %}

Option 2: Build Tool Integration

Use Vite, Webpack, or another bundler:

components/card.jinja
{#css /dist/card.css #}
{#js /dist/card.js #}

Your build tool will generates the files with hashes for cache-busting:

<link rel="stylesheet" href="/dist/card.abc123.css">
<script type="module" src="/dist/card.def456.js"></script>

Best Practices

1. Declare Third-Party Dependencies

{# ✅ Good - explicit dependencies #}
{#css https://cdn.example.com/library.css #}
{#import "./component-using-library.jinja" as Component #}

2. Keep Asset Files Small

Each component should have focused styles and scripts:

{# ✅ 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

/* ✅ 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:

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

{# card.jinja uses common.css #}
{# button.jinja uses common.css #}
{# page.jinja uses both #}

Results in:

<link rel="stylesheet" href="common.css">  <!-- Only once! -->
<link rel="stylesheet" href="card.css">
<link rel="stylesheet" href="button.css">

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.