How the template library works, how to add or change a template, and the rules a template must obey.
There are two ways in, and the difference is who owns the result:
| Libraries → Chart templates (§2) | Repository files (§3–§9) | |
|---|---|---|
| Who can add one | any signed-in user | whoever can merge to the repo |
| Who can edit it | its author, and nobody else | whoever can merge |
| Who can use it | everyone | everyone |
| Shown as | by <author> | built-in |
| Survives a redeploy | yes, untouched — the seeder never writes an authored row | yes, re-seeded from the file |
Use the tab for your own work. Use the repository for the templates the platform should ship with out of the box.
The technical ground truth for both — the exact params Apache Superset accepts, the render context, the helper list, the live-import proof against Apache Superset 6.1.0 — is superset-handlebars-findings.
1. What a template is
A template is a rendering override for Apache Superset and the HTML dashboard, not a chart type. Every chart keeps its standard ChartType (TABLE, KPI, BAR, …); pinning a template makes the Apache Superset exporter emit a chart of viz_type: handlebars whose HTML comes from the template's body instead of the type's own builder, and makes an HTML dashboard draw that chart with the template in the browser, from the rows embedded in the file (HTML dashboard §4, Template charts). One template serves both: the same body, the same { data: rows }, the same two helpers. Switching back to "Standard" simply clears the reference.
On an HTML dashboard a template is pinned from the chart's title or the Charts panel under the result, and becomes a new, free version of the dashboard. There, a template is drawn only on a chart of its own baseType; anywhere else the chart keeps its standard look.
Each template declares which standard type it belongs to (baseType) — that is how the preview's picker groups the library ("I chose Table → show me the Table templates") — and which result shape it iterates (queryMode):
| queryMode | The rows the template receives |
|---|---|
AGGREGATE | One row per group: the dimension column(s) plus one key per metric (sum__revenue, count, …) |
RAW | One row per record, keyed by the chart's projected column names |
A design may name one per chart type (visuals.chart, HTML dashboard §5a): a chart nobody pinned then takes it, when it fits — on HTML always, on Apache Superset where the Handlebars tab is switched on, since that is the deployment saying its Superset renders them. A pin by a person or the agent always wins, and a design can name only a template its author may use.
1a. The Handlebars tab in an Apache Superset dashboard
Every dashboard generated for Apache Superset ends with one extra tab, Handlebars, holding copies of up to seven of its charts, each drawn by one of the built-in templates: a row of cards, a trend card, a ranked list, category tiles, two measures by period, a grouped table and a raw-row table. The charts on the other tabs are unchanged — the standard Apache Superset visualisations they have always been. The point is to let you judge the look on your own data, after the import, instead of before it.
The charts on it draw nothing until your Apache Superset allows two things — see What your Apache Superset has to allow below, which the tab also names in its own closing note, so you do not have to come back here to find out why it looks empty. A deployment that does not want the offer at all switches it off with SUPERSET_HANDLEBARS_TAB=false.
It opens with a block of text, Why these charts: one sentence on what the dashboard is for and one per chart on what that chart answers — the same explanation the HTML dashboard shows on its Notes tab. It closes with a note saying what the tab is and naming the two Apache Superset settings below, so a reader who imported the bundle without them can see why the charts are empty without leaving the dashboard.
What you can do with it once the bundle is imported:
- Keep it. Nothing else depends on it.
- Change a chart's look. Open it in the chart editor; the template body and its CSS are the Handlebars Template and CSS Styles controls under Customize. Edit them there and save — that changes this one chart, not the library.
- Delete the tab. Edit the dashboard, remove the tab. The charts it held were copies, so the rest of the dashboard is complete without them.
You can also pin a template to a single chart instead, from the dashboard preview after a run — that needs the same two Apache Superset settings, and nothing else.
What your Apache Superset has to allow
Measured on a stock Apache Superset 6.1.0, a Handlebars chart needs two settings changed before it shows anything. Neither can be set from here — the bundle is a file you import into your own Superset — so check them before judging the tab:
| Symptom in the chart | Setting |
|---|---|
| "Evaluating a string as JavaScript violates the following Content Security Policy directive…" | Apache Superset's default policy forbids unsafe-eval, and Handlebars compiles a template by evaluating a string. Add 'unsafe-eval' to script-src in TALISMAN_CONFIG, or set TALISMAN_ENABLED=False. |
| Unstyled rows, with the template's CSS printed as text underneath | The default HTML sanitiser strips every attribute — class and style — and drops the <style> block. Extend HTML_SANITIZATION_SCHEMA_EXTENSIONS to allow them, for example {"tagNames": ["style"], "attributes": {"*": ["className", "class", "style"]}}. |
Both loosen a security setting your Apache Superset chose on purpose. Weigh that: if you would rather not, delete the tab — or use the HTML dashboard, which draws the very same templates with no such constraint.
One more CSS quirk, once the sanitiser lets your stylesheet through: Apache Superset drops a whole rule that contains a property it does not recognise, not just that property. Keep an unusual declaration in a rule of its own, or a neighbour like display: flex goes down with it.
Two further limits, whatever the settings. A Handlebars chart is rendered HTML, so it has no tooltips, no zoom, and it does not emit cross-filters the way a native visualisation does — "two measures by period" is a table of periods, not a two-axis chart. And the plugin has no time-grain control, so a chart grouped by a raw timestamp column shows one row per timestamp; a period column (a month, a date) is what reads.
2. Adding templates from the UI
Open Libraries, then Chart templates (labelled Apache Superset and HTML dashboards). It lists the whole shared library — yours first, then everyone else's — with a search box (name, description, "when to use", keywords, author) and a chart-type filter.
Create one: New template opens an editor with the form on the left and a live preview on the right, rendered on sample data through the same pipeline the real dashboard preview uses. What you see is what someone picking your template will see, including the empty-rows case (there is a toggle for it — that is where most templates break).
Fill in:
- Name — what the picker shows. Two users may both have a "Status table"; names are not unique.
- Description — one line: what it looks like.
- When to use it — the SKILL-style half: when to pick it and what it expects ("one dimension and 1–3 metrics, up to about twenty rows"). Shown beside the preview when someone is choosing, and read by the HTML design agent, which decides from this sentence alone whether to pin your template on a chart of the first version — so write it for a reader who has not seen the template: say what shape of chart it suits and what it needs, not what it looks like. Good: "Use for a handful of categories that each deserve their own number; expects one dimension and one metric, best under about ten values." Bad: "Nice rounded tiles."
- Keywords — the words others will search for.
- Chart type — only charts of this type are offered your template. Picking
TABLEmeans it appears when someone clicks a table, and never when they click a pie. - Rows —
AGGREGATE(grouped: dimension + one key per metric) orRAW(one row per record). - Handlebars and CSS — the rules in §6 apply exactly as they do to repository files, and the editor checks them before it sends anything.
Edit or delete — only on your own rows. Deleting is safe: any chart still pinned to the template falls back to its standard chart type on the next export.
Copy — available on every template, including the built-ins. This is how you start from one that nearly works: it pre-fills the editor with the bodies and fields, clears the identity, and saves as a new template of yours. It is also the answer to "I want to tweak a built-in", which the API refuses outright (409 template-not-editable).
From a chart: the chart picker shows by <author> under each template, and — on your own — an Edit this template shortcut that jumps straight into this editor.
Private or Public — every template you create carries a visibility flag, and it starts Private: creating is not publishing. A private template appears in your library only, and only you can pin it to a chart. Switch it to Public and everyone sees and uses it — but it stays yours to change or delete, and a colleague still gets 404 on a PUT.
Publishing is safe in one direction only: once somebody's dashboard pins your template, taking it private again does not break their export. Visibility decides who may choose a template, never what is delivered — the same rule that lets a disabled Power BI theme keep exporting.
There is a limit of 10 templates per user, per library: ten Handlebars charts, ten Apache Superset themes, ten Power BI Dashboard-Templates, ten Power BI Visual-Templates. The library header shows your count ("3 of 10 yours") and the New button is disabled at ten. The eleventh is refused with the library and the number in the message; delete one you no longer use.
Everything here is free: authoring a template runs no AI and consumes no coins.
3. The repository path (built-in templates)
The seven templates that ship with the platform live as files and are managed by the seeder. Nothing in the UI can change them.
4. Where the built-in templates live
generator/src/main/resources/chart-templates/<name>.json
The file name (minus .json) is the template's stable identity. ChartTemplateSeeder upserts every file into the chart_template table on each boot of the Superset engine:
- new file → inserted,
enabled = true - changed body (SHA-256 over handlebars + css) → updated in place
- unchanged → no-op
enabledis never touched by reseeding. Disabling a template is an operator action in the database (UPDATE chart_template SET enabled = false WHERE name = '…'), and it survives every redeploy. A disabled template stops being offered and stops resolving at export — a chart still pinned to it silently falls back to its standard type rather than failing the bundle.
Deleting a file does not delete the row; the template just stops being updated. Remove the row by hand if it should disappear entirely.
The seeder also never touches a row that has an author — a user's template can never be overwritten by a redeploy, and the two paths cannot collide (authored names are generated with a u- prefix).
5. The file format
{
"displayName": "Clean table",
"description": "Grouped rows as a plain table; headers come from the query, numbers are right-aligned.",
"baseType": "TABLE",
"queryMode": "AGGREGATE",
"handlebars": "<table class=\"hb-table\">\n ...\n</table>\n",
"css": ".hb-table { width: 100%; }\n"
}
| Field | Required | Rules |
|---|---|---|
displayName | yes | ≤ 96 chars; what the picker shows |
description | no | ≤ 255 chars; one line, what it looks like |
whenToUse | yes for shipped files | ≤ 1000 chars; when to pick it and what it expects |
keywords | yes for shipped files | ≤ 255 chars; the words people will search for |
baseType | yes | one of KPI, KPI_TREND, LINE, BAR, PIE, TABLE, SCATTER, FUNNEL, AREA. Not PIVOT: a matrix renders its own crosstab, so a template would replace it rather than restyle it, and both the API and the preview refuse the pin. |
queryMode | yes | AGGREGATE or RAW |
handlebars | yes | the template body — see §6 |
css | no | plain CSS, self-contained |
6. Rules every template body must obey
The seeder (and the write API, using the same shared rules) rejects (skips, with an error log naming the file) any body containing:
<script,javascript:, or an inline event handler (onclick=etc.) — the body is rendered in a sandboxed iframe in Studio and sanitised by Apache Superset; anything active is refused at the door rather than stripped inconsistently in two places@importorurl(http…)/url(//…)— the platform is air-gapped; a template must never pull an external resource- Unescaped output,
{{{…}}}or{{& …}}— every value is escaped by default, and the HTML dashboard inserts the rendered template into its page, so a value must never be able to become markup there
Beyond the hard rejections:
- The outer loop must be rooted at
data. The plugin wraps the rows one level deep, so{{#each data}} … {{/each}}iterates the rows and{{#each data.[0]}}iterates the first row's keys (useful for headers and cards). A loop over the root iterates the wrapper object and renders nothing useful. - Metric keys are the dataset metric names —
sum__revenue,avg__price,count. Prefer iterating{{#each this}}inside a row with{{@key}}/{{this}}over hard-coding column names, so the template works on any dataset. - Only two helpers are allowed:
formatNumberanddateFormat(hash-arg style:{{formatNumber this locale="de-DE"}},{{dateFormat this format="YYYY-MM"}}). Apache Superset registers more (stringify,parseJson, all ofjust-handlebars-helpers), but the Studio preview deliberately mirrors only these two — a template using anything else would look broken in the preview or, worse, fine in the preview and different in Apache Superset. Handlebars is logic-less: a template cannot compute a max or a percentage across rows, so don't design one that needs to (this is why there is no "proportional bar" template). - Handle the empty case: with zero rows the template must render empty, not throw. (A KPI-shaped chart in the HTML dashboard always gets its one row, with zeros.)
- Filtering the HTML dashboard from a template (optional). Write
data-dg-value="{{this}}"on the element that stands for one group — the dimension's value — and a click on it in an HTML dashboard filters every other chart by that value, as a bar click does; the selected elements get the classdg-selected, so style.your-class.dg-selected. It works on anAGGREGATEtemplate whose chart has a text dimension. Apache Superset ignores the attribute, so the template stays valid there.tiles-categoryis the worked example:<div class="hb-tile" {{#each this}}{{#if @first}}data-dg-value="{{this}}"{{/if}}{{/each}}>. RAWrows in an HTML dashboard are capped at 500.
7. The two guardrails (run in CI)
Adding a broken file fails the build, not a user's preview:
generator—ChartTemplateAssetsTest: JSON parses, required fields present, enums valid, no forbidden content, loop rooted atdata. Run:./mvnw -pl generator -am test -Dtest=ChartTemplateAssetsTest -Dsurefire.failIfNoSpecifiedTests=false -DskipFrontend=truestudiofrontend —handlebarsLibrary.test: actually compiles and renders every body against sample data in the same runtime the preview uses, including the empty-rows case. Run:cd frontend && npm test -- handlebarsLibrary
8. Step by step: adding a built-in template
- Copy the closest existing file in
chart-templates— the seven shipped ones cover cards, tables (both query modes), a ranked list and category tiles. - Edit
displayName,description,baseType,queryMode, the body and the CSS. Keep class names prefixed (hb-…) so they cannot collide with Apache Superset's own styles. - Run both guardrail tests (§5).
- Restart the generator engine (or
docker compose up --build generator) — the seeder logsChart template '<name>' seeded. - Check it in Studio: generate an Apache Superset dashboard, open the preview, click a chart of the template's
baseType— the template appears in the Library group with a live mini-preview on the chart's own data. - For a change that touches query params rather than just markup, do the real check once: import an exported bundle into a local Apache Superset (
docker run -p 8088:8088 -e SUPERSET_SECRET_KEY=… apache/superset, thensuperset db upgrade/fab create-admin/superset initinside the container) and confirm the chart renders. Record surprises insuperset-handlebars-findings.
9. Editing and removing a built-in
- Edit = change the JSON file; the checksum changes and the next boot updates the row. Charts already pinned to the template pick the new body up on their next export (the bundle is rebuilt from the stored design on download).
- Disable =
enabled = falsein the DB (operator action, survives reseeds). Pinned charts downgrade to their standard type at export; nothing breaks. - Rename = don't. The file name is the identity that pinned charts reference. A rename is a new template plus an orphaned old row, and every chart pinned to the old name silently loses its template. Change
displayNameinstead — that is what users see.
DASH:Gen