Templates#

If you want to provide a panel app or a dashboard as a bokeh application, it is rendered in a standard template that refers to the JS and CSS resources as well as the actual panel object. If you want to adapt the layout of the provided app or if you want to provide several separate panels in one app, the Template component of Panel allows you to adapt this standard template.

Such a template is defined with Jinja, whereby you can extend or even completely replace the standard template. Here is an example:

<!DOCTYPE html>
<html lang="en">
{% block head %}
<head>
    {% block inner_head %}
    <meta charset="utf-8">
    <title>{% block title %}{{ title | e if title else "Panel App" }}{% endblock %}</title>
    {% block preamble %}{% endblock %}
    {% block resources %}
        {% block css_resources %}
        {{ bokeh_css | indent(8) if bokeh_css }}
        {% endblock %}
        {% block js_resources %}
        {{ bokeh_js | indent(8) if bokeh_js }}
        {% endblock %}
    {% endblock %}
    {% block postamble %}{% endblock %}
    {% endblock %}
</head>
{% endblock %}
{% block body %}
<body>
    {% block inner_body %}
    {% block contents %}
        {% for doc in docs %}
        {{ embed(doc) if doc.elementid }}
        {% for root in doc.roots %}
            {{ embed(root) | indent(10) }}
        {% endfor %}
        {% endfor %}
    {% endblock %}
    {{ plot_script | indent(8) }}
    {% endblock %}
</body>
{% endblock %}
</html>

The template defines a number of user-defined blocks that can be supplemented or overwritten by extends:

Use custom templates#

[1]:
import holoviews as hv
import panel as pn


pn.extension()

Once we have Panel loaded, we can start defining a custom template. It is usually easy to customise an existing template by overwriting certain blocks. With {% extends base %} we declare that we are only expanding an existing template and not defining a new one.

In the following case, we are expanding the postamble block of the header to load an additional resource and the contents block to redefine the arrangement of the components:

[2]:
template = """
{% extends base %}

<!-- head -->
{% block postamble %}
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css">
{% endblock %}

<!-- body -->
{% block contents %}
<h1>Custom template for multiple apps</h1>
<p>This is a Panel app with a custom template allowing us to compose multiple Panel objects into a single HTML document.</p>
<br>
<div class="container">
  <div class="row">
    <div class="col-sm">
      {{ embed(roots.A) }}
    </div>
    <div class="col-sm">
      {{ embed(roots.B) }}
    </div>
  </div>
</div>
{% endblock %}
"""

Using the embed macro, we have defined two different roots in the template. In order to be able to render the template, we must first create the pn.Template object with the HTML template and then integrate the two roots objects.

[3]:
tmpl = pn.Template(template)

tmpl.add_panel("A", hv.Curve([1, 2, 3]))
tmpl.add_panel("B", hv.Curve([1, 2, 3]))

tmpl.servable()
[3]:

Custom template for multiple apps

This is a Panel app with a custom template allowing us to compose multiple Panel objects into a single HTML document.


A button is rendered in the notebook with which you can start a local server to check whether the output meets your expectations.

If the template is larger, it is often easier to create it in a separate file. You can use the Jinja2 template loading mechanism by defining an environment together with a loader:

[4]:
from jinja2 import Environment, FileSystemLoader


env = Environment(loader=FileSystemLoader("."))
jinja_template = env.get_template("sample_template.html")

tmpl = pn.Template(jinja_template)

tmpl.add_panel("A", hv.Curve([1, 2, 3]))
tmpl.add_panel("B", hv.Curve([1, 2, 3]))

tmpl
[4]:

Custom template for multiple panels

This is a Panel app with a custom template allowing us to compose multiple Panel objects into a single HTML document.