Skip to content

Rendering JS / CSS

Introductionยค

Components consist of 3 parts - HTML, JS and CSS.

Handling of HTML is straightforward - it is rendered as is, and inserted where the {% component %} tag is.

However, handling of JS and CSS is more complex:

  • JS and CSS is are inserted elsewhere in the HTML. As a best practice, JS is placed in the <body> HTML tag, and CSS in the <head>.
  • Multiple components may use the same JS and CSS files. We don't want to load the same files multiple times.
  • Fetching of JS and CSS may block the page, so the JS / CSS should be embedded in the HTML.
  • Components inserted as HTML fragments need different handling for JS and CSS.

Default JS / CSS locationsยค

If your components use JS and CSS then, by default, the JS and CSS will be automatically inserted into the HTML:

  • CSS styles will be inserted at the end of the <head>
  • JS scripts will be inserted at the end of the <body>

If you want to place the dependencies elsewhere in the HTML, you can override the locations by inserting following Django template tags:

So if you have a component with JS and CSS:

from django_components import Component, types

class MyButton(Component):
    template: types.django_html = """
        <button class="my-button">
            Click me!
        </button>
    """

    js: types.js = """
        for (const btnEl of document.querySelectorAll(".my-button")) {
            btnEl.addEventListener("click", () => {
                console.log("BUTTON CLICKED!");
            });
        }
    """

    css: types.css """
        .my-button {
            background: green;
        }
    """

    class Media:
        js = ["/extra/script.js"]
        css = ["/extra/style.css"]

Then:

And if you don't specify {% component_dependencies %} tags, it is the equivalent of:

<!doctype html>
<html>
  <head>
    <title>MyPage</title>
    ...
    {% component_css_dependencies %}
  </head>
  <body>
    <main>
      ...
    </main>
    {% component_js_dependencies %}
  </body>
</html>

Warning

If the rendered HTML does NOT contain neither {% component_dependencies %} template tags, nor <head> and <body> HTML tags, then the JS and CSS will NOT be inserted!

To force the JS and CSS to be inserted, use the "append" or "prepend" strategies.

Dependencies strategiesยค

The rendered HTML may be used in different contexts (browser, email, etc). If your components use JS and CSS scripts, you may need to handle them differently.

The different ways for handling JS / CSS are called "dependencies strategies".

render() and render_to_response() accept a deps_strategy parameter, which controls where and how the JS / CSS are inserted into the HTML.

main_page = MainPage.render(deps_strategy="document")
fragment = MyComponent.render_to_response(deps_strategy="fragment")

The deps_strategy parameter is set at the root of a component render tree, which is why it is not available for the {% component %} tag.

When you use Django's django.shortcuts.render() or Template.render() to render templates, you can't directly set the deps_strategy parameter.

In this case, you can set the deps_strategy with the DJC_DEPS_STRATEGY context variable.

from django.template.context import Context
from django.shortcuts import render

ctx = Context({"DJC_DEPS_STRATEGY": "fragment"})
fragment = render(request, "my_component.html", ctx=ctx)

Info

The deps_strategy parameter is ultimately passed to render_dependencies().

Why is deps_strategy required?

This is a technical limitation of the current implementation.

When a component is rendered, django-components embeds metadata about the component's JS and CSS into the HTML.

This way we can compose components together, and know which JS / CSS dependencies are needed.

As the last step of rendering, django-components extracts this metadata and uses a selected strategy to insert the JS / CSS into the HTML.

There are six dependencies strategies:

  • document (default for top-level)
    • Smartly inserts JS / CSS into placeholders or into <head> and <body> tags.
    • Requires the HTML to be rendered in a JS-enabled browser.
    • Inserts extra script for managing fragments.
  • fragment
    • A lightweight HTML fragment to be inserted into a document with AJAX.
    • Fragment will fetch its own JS / CSS dependencies when inserted into the page.
    • Requires the HTML to be rendered in a JS-enabled browser.
  • simple
    • Smartly insert JS / CSS into placeholders or into <head> and <body> tags.
    • No extra script loaded.
  • prepend
    • Insert JS / CSS before the rendered HTML.
    • No extra script loaded.
  • append
    • Insert JS / CSS after the rendered HTML.
    • No extra script loaded.
  • ignore (default when nested)
    • HTML is left as-is. You can still process it with a different strategy later with render_dependencies().
    • Used for inserting rendered HTML into other components.

documentยค

deps_strategy="document" is the default. Use this if you are rendering a whole page, or if no other option suits better.

html = Button.render(deps_strategy="document")

When you render a component tree with the "document" strategy, it is expected that:

  • The HTML will be rendered at page load.
  • The HTML will be inserted into a page / browser where JS can be executed.

Location:

JS and CSS is inserted:

  • Preferentially into JS / CSS placeholders like {% component_js_dependencies %}
  • Otherwise, JS into <body> element, and CSS into <head> element
  • If neither found, JS / CSS are NOT inserted

Included scripts:

For the "document" strategy, the JS and CSS is set up to avoid any delays when the end user loads the page in the browser:

  • Components' primary JS and CSS scripts (Component.js and Component.css) - fully inlined:

    <script>
        console.log("Hello from Button!");
    </script>
    <style>
        .button {
            background-color: blue;
        }
    </style>
    
  • Components' secondary JS and CSS scripts (Component.Media) - inserted as links:

    <link rel="stylesheet" href="https://example.com/styles.css" />
    <script src="https://example.com/script.js"></script>
    
  • A JS script is injected to manage component dependencies, enabling lazy loading of JS and CSS for HTML fragments.

How the dependency manager works

The dependency manager is a JS script that keeps track of all the JS and CSS dependencies that have already been loaded.

When a fragment is inserted into the page, it will also insert a JSON <script> tag with fragment metadata.

The dependency manager will pick up on that, and check which scripts the fragment needs.

It will then fetch only the scripts that haven't been loaded yet.

fragmentยค

deps_strategy="fragment" is used when rendering a piece of HTML that will be inserted into a page:

fragment = MyComponent.render(deps_strategy="fragment")

The HTML of fragments is very lightweight because it doesn't include the JS and CSS scripts of the rendered components.

With fragments, even if a component has JS and CSS, you can insert the same component into a page hundreds of times, and the JS and CSS will only ever be loaded once.

This is intended for dynamic content that's loaded with AJAX after the initial page load, such as with jQuery, HTMX, AlpineJS or similar libraries.

Location:

None. The fragment's JS and CSS files will be loaded dynamically into the page.

Included scripts:

  • A special JSON <script> tag that tells the dependency manager what JS and CSS to load.

simpleยค

deps_strategy="simple" is used either for non-browser use cases, or when you don't want to use the dependency manager.

Practically, this is the same as the "document" strategy, except that the dependency manager is not used.

html = MyComponent.render(deps_strategy="simple")

Location:

JS and CSS is inserted:

  • Preferentially into JS / CSS placeholders like {% component_js_dependencies %}
  • Otherwise, JS into <body> element, and CSS into <head> element
  • If neither found, JS / CSS are NOT inserted

Included scripts:

  • Components' primary JS and CSS scripts (Component.js and Component.css) - fully inlined:

    <script>
        console.log("Hello from Button!");
    </script>
    <style>
        .button {
            background-color: blue;
        }
    </style>
    
  • Components' secondary JS and CSS scripts (Component.Media) - inserted as links:

    <link rel="stylesheet" href="https://example.com/styles.css" />
    <script src="https://example.com/script.js"></script>
    
  • No extra scripts are inserted.

prependยค

This is the same as "simple", but placeholders like {% component_js_dependencies %} and HTML tags <head> and <body> are all ignored. The JS and CSS are always inserted before the rendered content.

html = MyComponent.render(deps_strategy="prepend")

Location:

JS and CSS is always inserted before the rendered content.

Included scripts:

Same as for the "simple" strategy.

appendยค

This is the same as "simple", but placeholders like {% component_js_dependencies %} and HTML tags <head> and <body> are all ignored. The JS and CSS are always inserted after the rendered content.

html = MyComponent.render(deps_strategy="append")

Location:

JS and CSS is always inserted after the rendered content.

Included scripts:

Same as for the "simple" strategy.

ignoreยค

deps_strategy="ignore" is used when you do NOT want to process JS and CSS of the rendered HTML.

html = MyComponent.render(deps_strategy="ignore")

The rendered HTML is left as-is. You can still process it with a different strategy later with render_dependencies().

This is useful when you want to insert rendered HTML into another component.

html = MyComponent.render(deps_strategy="ignore")
html = AnotherComponent.render(slots={"content": html})

Behavior inside get_template_data()ยค

When you pre-render a component in Python, and pass it into another component's get_template_data(), you should pass deps_strategy="ignore" to the render function to avoid rendering the dependencies twice.

class Outer(Component):
    def get_template_data(self, args, kwargs, slots, context):
        content = Inner.render(deps_strategy="ignore")
        return {"content": content}

django-components makes this easier for you. When you are inside another component, the deps_strategy defaults to "ignore" automatically.

Top-level renders still default to "document".

class Outer(Component):
    def get_template_data(self, args, kwargs, slots, context):
        # defaults to "ignore" when nested
        content = Inner.render()
        return {"content": content}

# `deps_strategy` defaults to "document" when top-level
rendered = Outer.render()

Modifying JS / CSS scriptsยค

Before JS and CSS dependencies are rendered into <script>, <style>, and <link> tags, they can be modified in two places:

  1. Per component: Each component's dependencies are passed through that component's Component.on_dependencies() hook. This includes the component's Component.js / Component.css and JS/CSS variables. See Component hooks: on_dependencies.
  2. Globally: The combined list of all dependencies is then passed through the ComponentExtension.on_dependencies extension hook.

You can use these hooks to add, remove, or modify Script and Style objects, so that the final HTML reflects your changes.

Use cases include:

  • Adding a custom <script> or <style> (e.g. analytics, CSP nonce)
  • Removing or reordering scripts or styles
  • Changing attributes on scripts (e.g. type="module") or styles

Component hook: on_dependenciesยค

Component.on_dependencies is a classmethod hook that allows you to modify the JS / CSS dependencies emitted by this component only.

These are the <script> and <style> tags that will be rendered for this component.

The JS / CSS are available as lists of Script and Style objects.

Example:

from django_components import Component, Script, Style

class MyButton(Component):
    # ...

    @classmethod
    def on_dependencies(cls, scripts, styles):
        # Add a nonce to every inline style for this component
        for style in styles:
            if style.content and "nonce" not in style.attrs:
                style.attrs["nonce"] = get_current_nonce()
        return (scripts, styles)

Full details and more examples are in Component hooks.

Extension hook: on_dependenciesยค

In the ComponentExtension.on_dependencies hook, you can modify the entire list of Script and Style objects that will be rendered (all components and Media).

Return a tuple (scripts, styles) to replace the lists that will be rendered, or None to leave dependencies unchanged.

Example:

from django_components import ComponentExtension, OnDependenciesContext, Script, Style

class MyExtension(ComponentExtension):
    name = "my_extension"

    def on_dependencies(self, ctx: OnDependenciesContext):
        scripts = list(ctx.scripts)
        styles = list(ctx.styles)
        # Add a nonce to every inline style
        for style in styles:
            if style.content and "nonce" not in style.attrs:
                style.attrs["nonce"] = get_current_nonce()
        return (scripts, styles)

See Extensions for how to register and configure extensions.

Wrapping inline JS in a self-executing functionยค

By default, inline JavaScript in component scripts is wrapped in a self-executing (IIFE) function so that variables do not leak into the global scope.

Whether a given <script> is wrapped depends on Script.wrap (default True) and the script's type attribute.

To disable wrapping for a specific script, set Script.wrap to False.

script = Script(
    content="console.log('Hello');",
    wrap=False,
)

Wrapping is applied when:

  • No type attribute, e.g. <script>
  • Empty type, e.g. <script type="">
  • JavaScript MIME types, e.g. type="text/javascript" or type="application/javascript"

Wrapping is NOT applied when:

Anything else is not wrapped. This includes:

  • type="module" - ES modules have their own scope
  • type="importmap" - import map JSON, not executable JS
  • type="speculationrules" - speculation rules JSON
  • type="application/json" or any other non-JS type - data blocks are not executed as script

Manually rendering JS / CSSยค

When rendering templates or components, django-components covers all the traditional ways how components or templates can be rendered:

This way you don't need to manually handle rendering of JS / CSS.

However, for advanced or low-level use cases, you may need to control when to render JS / CSS.

In such case you can directly pass rendered HTML to render_dependencies().

This function will extract all used components in the HTML string, and insert the components' JS and CSS based on given strategy.

Info

The truth is that all the methods listed above call render_dependencies() internally.

Example:

To see how render_dependencies() works, let's render a template with a component.

We will render it twice:

  • First time, we let template.render() handle the rendering.
  • Second time, we prevent template.render() from inserting the component's JS and CSS with deps_strategy="ignore".

    Instead, we pass the "unprocessed" HTML to render_dependencies() ourselves to insert the component's JS and CSS.

from django.template.base import Template
from django.template.context import Context
from django_components import render_dependencies

template = Template("""
    {% load component_tags %}
    <!doctype html>
    <html>
    <head>
        <title>MyPage</title>
    </head>
    <body>
        <main>
            {% component "my_button" %}
                Click me!
            {% endcomponent %}
        </main>
    </body>
    </html>
""")

rendered = template.render(Context({}))

rendered2_raw = template.render(Context({"DJC_DEPS_STRATEGY": "ignore"}))
rendered2 = render_dependencies(rendered2_raw)

assert rendered == rendered2

Same applies to other strategies and other methods of rendering:

raw_html = MyComponent.render(deps_strategy="ignore")
html = render_dependencies(raw_html, deps_strategy="document")

html2 = MyComponent.render(deps_strategy="document")

assert html == html2

HTML fragmentsยค

Django-components provides a seamless integration with HTML fragments with AJAX (HTML over the wire), whether you're using jQuery, HTMX, AlpineJS, vanilla JavaScript, or other.

This is achieved by the "fragment" strategy.

Read more about HTML fragments.