Skip to content

Template format

The files a Kinetra template is made of, and the rules the output follows when it draws one.

This page is for people who write or edit templates by hand, in Kinetra’s code editor or in their own editor. You don’t need it to use the built-in templates or AI templates.

Kinetra watches the project folder: save a file in another editor and the change shows up in Kinetra and on air. If an edit made outside Kinetra isn’t valid, the last good version stays on air.

A template is one folder in the project’s templates/ folder:

<project>/templates/<id>/
template.json the manifest: name, fields, variables, looks
variants/
<variantId>/
template.html this look's markup; its first element is the root
template.css this look's styles
<variantId>/
…
  • <id> is the template’s id: letters, digits, _ and -, 1–64 characters. Kinetra makes 10-character ids. Folders with other names are ignored.
  • Each look (variant) listed in template.json has a folder in variants/. A missing template.html or template.css counts as empty.
  • Files are UTF-8.
  • Images and video don’t live in the template. They live in the project’s assets/ folder and are used as /assets/<file> in field values.
  • Templates from older versions of Kinetra (HTML and CSS directly in the template folder) are converted to a single look called “Default” when you open the project. A backup is made first.
  • A template.json from a newer version of Kinetra is never overwritten. The template won’t open until you update Kinetra.
{
"schemaVersion": 2,
"name": "Lower third",
"description": "A name strap.",
"preset": null,
"fields": [],
"variables": [],
"autoOutMs": null,
"design": { "w": 960, "h": 200, "x": 115, "y": 783, "sizing": "fit" },
"defaultVariant": "default",
"variants": [],
"updatedAt": "2026-10-10T12:00:00Z"
}
Key Type Rules
schemaVersion number 2.
name string 1–100 characters after trimming.
description string Up to 1000 characters. Optional.
preset {id, version} or null Set when the template was copied from a built-in template, so Kinetra can offer newer looks. Use null for your own templates.
fields array The content an operator fills in. Up to 64. See Fields.
variables array Styling knobs exposed as CSS custom properties. Up to 32. See Variables.
autoOutMs number or null Take the graphic off air this many milliseconds after it goes on, 500–600000. null keeps it on air until it is cleared.
design object Optional design box. Leave it out for a full-screen template. See Design box.
defaultVariant string The look a layer shows unless it picks another. Must be one of variants.
variants array The looks, 1–12. See Looks (variants).
updatedAt string Written by Kinetra on every save.

Keys of fields, variables and list columns start with a lowercase letter, followed by up to 31 lowercase letters, digits, _ or -. A field and a variable can’t share a key.

Labels are what operators see. They are 1–100 characters after trimming.

The size the graphic is designed at, and where a new layer is placed.

Key Rules
w, h Whole numbers, 16–8192.
x, y Optional. A new layer’s top-left corner on a 1920×1080 screen, scaled to the screen’s size. Left out: centered.
sizing "fit" or "reflow": what new layers from this template use. Default "reflow".
  • fit (Scale to fit): the template is laid out at exactly w×h pixels, then scaled evenly (never stretched) and centered in the layer’s box. Use it for designed graphics such as lower thirds, bugs and titles.
  • reflow: the template fills the layer’s box and lays itself out. Use it for grids, tickers, schedules and sponsor walls, with container units and container queries.
  • Without design, a new layer fills the screen, and fit uses the screen’s size.
  • Make the design box hug the graphic, so resizing the layer resizes what you see.

Fields are the content: names, titles, images, rows, timers. They are declared once in template.json, shared by every look, and placed in the HTML with {{key}}.

{ "key": "name", "label": "Name", "type": "text", "default": "" }
Key Rules
key See the key rules above.
label 1–100 characters.
type One of the types below.
default A string: the value a new layer starts with.
columns list only: 1–16 {key, label, kind?}. kind is "text" (the default) or "image".
format clock only: the display format.

All values are strings. Every type except html is inserted as plain text.

Type Value Notes
text Plain text. One line in the operator’s form.
multiline Plain text with line breaks. Line breaks only show if your CSS sets white-space: pre-line or similar.
number A number as text. Shown as written.
color A color. Usually bound into a style attribute.
image A URL, usually /assets/<file>. Bind it into src. Only http(s), relative and data:image/… URLs are accepted.
video A URL, usually /assets/<file>. Bind it into a <video src>. Same URL rules.
html Markup. Inserted as markup, with scripts, event handlers and unsafe URLs removed. Only in text position; in an attribute it is plain text.
list A JSON array of rows. Repeated with data-each. See Lists.
clock No value. Shows the time of day.
timer A timer state (JSON). Counts up or down. Operators start, pause and reset it from Control.

List values are a JSON array of objects, one key per column, every column present:

[{"text":"Doors open 18:00"},{"text":"Bar closes 01:00"}]

At most 100 rows, 4 KB per cell and 64 KB in total. Image cells hold /assets/<file>. The default must be a valid list ("[]" for none).

When a list is bound to a data source, Kinetra can add row marks: _status (previous, now, next, delayed, upcoming or cancelled, on schedules), _current (true on a slideshow’s current row) and _duration (seconds). They show up as data-status, data-current and data-duration on the row element.

Clock format uses HH (00–23), mm, ss, :, . and spaces, up to 32 characters. Empty means HH:mm:ss.

Timer values look like this:

{"run": false, "at": 0, "base": 0, "dir": "down", "end": "stop"}

dir is "up" or "down". A countdown stops at zero; with "end": "hide" the graphic is taken off air at zero. Timers show as m:ss under an hour and h:mm:ss from an hour. The default is "" (a stopped count-up at 0:00) or a timer state; a default never starts running by itself.

Each layer keeps its own field values. A field with no value on a layer uses its default. A field can also be bound to a data source; that is set on the layer, not in the template.

Variables are styling knobs (colors, fonts, sizes, choices) that an operator can change per layer without touching code. Each variable becomes the CSS custom property --<key>, so var(--<key>) works anywhere in template.css, including :host.

{ "key": "accent", "label": "Accent", "type": "color", "default": "#33d6ff" }
{ "key": "font-heading", "label": "Heading font", "type": "font", "default": "Space Grotesk" }
{ "key": "radius", "label": "Corner radius", "type": "range",
"min": 0, "max": 24, "step": 1, "unit": "px", "default": "2" }
{ "key": "align", "label": "Alignment", "type": "select",
"options": [{ "value": "left", "label": "Left" }, { "value": "center", "label": "Center" }],
"default": "left" }

Every value, including the default, is checked and rewritten by Kinetra before it reaches CSS.

Type Extra keys Valid values Value of --<key>
color none #rgb, #rrggbb, #rrggbbaa or transparent The value in lowercase.
font none One family from the font list below The family with fallbacks, for example "Barlow", system-ui, sans-serif.
range min, max, step, unit (up to 8 lowercase letters or %, "" for none) A plain number from min to max The number plus the unit, for example 12px.
select options: 1–64 {value, label} One of the option values The option value.

A layer’s value comes from, in order: the layer’s own setting, the project’s brand, the look’s vars, then the variable’s default. The brand fills only these keys: accent, secondary, text, plate, font-heading and font-body. It also fills an empty logo image field with the brand logo. Use these keys if a template should follow the project’s brand (see Templates and styles).

Templates can only use these families. Kinetra serves them itself, so every OBS or vMix computer shows the same font:

Barlow, Barlow Condensed, Oswald, Montserrat, IBM Plex Sans, IBM Plex Sans Condensed, Source Serif 4, Space Grotesk, Manrope, Archivo, Playfair Display, Jost, Instrument Sans, Plus Jakarta Sans, Outfit, Roboto Mono, IBM Plex Mono.

Weights 400, 600 and 700 are included for every family; some include more. Only some families have italics. Inter is not available: a template that names Inter gets IBM Plex Sans.

A look is one design of the template: its own HTML and CSS and its own variable values. All looks share the template’s fields and variables. Typical looks: Default, Compact, Sponsor.

"defaultVariant": "default",
"variants": [
{ "id": "default", "name": "Default", "vars": {} },
{ "id": "compact", "name": "Compact", "vars": { "radius": "0", "accent": "#e63946" } }
]
Key Rules
id Starts with a lowercase letter, then up to 31 lowercase letters, digits or - (no _, because it is also the folder name). Unique.
name 1–40 characters, unique ignoring case.
vars This look’s values for declared variables. Missing keys use the variable’s default.
  • A template has 1–12 looks. The last one can’t be deleted.
  • A look doesn’t have to use every field. Values stay on the layer, so switching to a look that ignores a field and back keeps the value.
  • A layer that names a look that no longer exists shows the default look.
  • The first element of template.html is the root. Kinetra sets data-state="in" on it when the graphic goes on air and data-state="out" when it goes off. Write exactly one top-level element.
  • Each layer is drawn in its own shadow root: the template’s CSS can’t affect anything else, and nothing else affects it.
  • template.html can be up to 256 KB.

{{key}} (or {{ key }}) is replaced by the field’s value, in text and in attribute values:

<div class="name">{{name}}</div>
<div class="role">{{title}} · {{company}}</div>
<img class="photo" src="{{photo}}" alt="">
<div class="chip" data-status="{{status}}"></div>
  • Values are inserted as text, except html fields.
  • A placeholder that names no field shows nothing.
  • clock and timer placeholders show the time and update once a second while on air.
  • Changing a value updates only that text or attribute. Running animations don’t restart.
Attribute Rule
on* (event handlers), srcdoc Placeholders are never filled in.
src, href, srcset, poster, xlink:href, action, formaction The value must be http(s), relative or data:image/…. Anything else becomes empty.
style Each value must be only letters, digits, spaces and # _ . % + -, up to 64 characters: colors, numbers, lengths and plain words. No url(), brackets, colons, semicolons or quotes. Anything else becomes empty. Example: style="--team: {{color}}".
everything else The value as given.

An element with data-each="<listField>" is repeated once for each row of that list field.

<ul class="rows">
<li class="row" data-each="items" data-limit="8">
<span class="n">{{#}}</span>
<span class="t">{{.title}}</span>
<img src="{{.logo}}" alt="">
<span class="when">{{start}}</span>
</li>
</ul>
Inside a row Meaning
{{.col}} This row’s value in column col.
{{#}} The row number, starting at 1.
{{key}} An ordinary field, the same in every row.
  • data-limit="<n>" shows at most n rows.
  • Each row gets style="--i: <index>" (starting at 0) for staggered animations, and data-row="in" when it is added. A row removed while on air gets data-row="out" and disappears when its animation ends.
  • Row marks become attributes on the row: data-status="now", data-current="true", data-duration="8".
  • The row’s parent gets data-count="<n>" and --n: <n>, and data-empty when there are no rows.
  • data-each doesn’t work on the root element or inside another data-each. Two separate row templates for the same list (for example a ticker’s repeated run) are fine.
Attribute Set by Meaning
data-state="in" / "out" Kinetra, on the root On air / going off air.
data-empty Kinetra On an element whose filled-in text and URLs are all empty, and on a list’s parent with no rows. Use it to hide missing parts: .photo[data-empty] { display: none }.
data-fit You Shrinks the text until it fits the element’s width in at most data-fit-lines lines (default 1), down to data-fit-min times the CSS size (default 0.6).
data-row, data-count, --i, --n Kinetra Lists.

Don’t set data-state or data-row yourself.

template.css can be up to 256 KB.

You can use:

What Where
--<key> for every variable Set on the host and inherited everywhere.
--auto-out-ms (for example 5000ms) On the host when the template has autoOutMs. Handy for a progress bar that drains while on air.
:host The layer itself, placed at the layer’s box. Overflow is visible, so out-animations can leave the box.
Container queries With fit, put container-type: size on your root to query the design box. With reflow, the root fills the layer box.
The fonts listed above Name them in font-family or font, directly or through a font variable. Kinetra loads them before the graphic plays in (waiting at most 1.5 seconds).

Anything else that the Chromium-based browser in OBS and vMix supports works: layout, color-mix(), container units, @keyframes, transitions, :has() and custom properties.

Don’t use:

Don’t Why
url(…) and @import Templates load nothing from outside. Images come in through image fields.
@font-face It doesn’t work inside a shadow root. Use the fonts above.
@media (prefers-reduced-motion) Windows’ “Animation effects: off” setting turns this on in OBS and vMix and would freeze your graphics.
Other fonts, or Inter They aren’t included, so the output would show a fallback font.
A weight or italic the family doesn’t include The browser fakes it.

Tips: animate only transform and opacity. Keep the important part of a fit graphic inside its design box. Hide missing parts with data-empty or :empty.

  1. In. When the graphic goes on air, the root gets data-state="in". Put the in-animation on .root[data-state="in"], with animation-fill-mode: both so it starts hidden.
  2. Out. When it goes off air, the root gets data-state="out". Put the out-animation on .root[data-state="out"], with forwards so it stays hidden. The layer is removed when its animations finish, after at most 3 seconds.
  3. No out-animation means the graphic disappears at once.
  4. Updates on air never replay the in-animation. Endless animations, such as a ticker’s scroll, keep running.
  5. Switching looks on air crossfades in about 300 ms, or plays the old look’s out and the new look’s in.
  6. PANIC removes the graphic at once, with no out-animation.
  7. Preview shows graphics settled, without in and out animations.

Templates need no JavaScript: clocks, timers and look changes are handled by Kinetra.

Kinetra checks every template when it is saved (and every AI result) and refuses it with “Templates can’t contain …” if it has:

  • JavaScript: <script> elements, on… event handler attributes, and javascript:, vbscript: or data:text/html URLs.
  • Embedded pages and forms: <iframe>, <frame>, <frameset>, <object>, <embed>, <applet>, <portal>, <form>, and the srcdoc and formaction attributes.
  • Page-level tags: <link>, <meta> and <base>.
  • Anything from outside: an attribute that starts with http://, https:// or //, and in CSS url(), @import and @font-face. Data from the internet is fetched by Kinetra and arrives as field values; images are imported into the project.
  • Old CSS tricks: expression(, -moz-binding and behavior:.

The rules in CSS under “Don’t use” (reduced motion, other fonts, missing weights) aren’t checked on save. Breaking them doesn’t stop the save, but the graphic won’t look right on air.

Don’t put anything private in a template: templates are copied into exports and backups.

Limit Value
Template id Letters, digits, _, -; 1–64 characters
Name / description 1–100 / up to 1000 characters
Field, variable and column keys Lowercase letter first, then up to 31 of a–z 0–9 _ -; fields and variables can’t share a key
Labels 1–100 characters
Fields / variables / looks Up to 64 / up to 32 / 1–12
Look id / name Lowercase letter first, then up to 31 of a–z 0–9 - / 1–40 characters
template.html, template.css Up to 256 KB each, per look
Select options 1–64
List 1–16 columns, up to 100 rows, 4 KB per cell, 64 KB in total
Clock format HH, mm, ss, :, . and spaces; up to 32 characters
Timer Up to 1000 hours
autoOutMs 500–600000
Design box 16–8192 pixels per side
Out-animation Removed after at most 3 seconds

If template.json has an error, Kinetra won’t save it and the editor says what to fix.

A lower third with a name, a role, an optional photo, topic tags, two looks and a progress bar. It goes off air after 8 seconds and scales to fit a 960×200 box at the bottom left.

templates/k3m9x2p7qa/template.json:

{
"schemaVersion": 2,
"name": "Speaker strap",
"description": "Name, role, photo and topic tags.",
"preset": null,
"fields": [
{ "key": "name", "label": "Name", "type": "text", "default": "" },
{ "key": "role", "label": "Role", "type": "text", "default": "" },
{ "key": "photo", "label": "Photo", "type": "image", "default": "" },
{
"key": "tags",
"label": "Topics",
"type": "list",
"columns": [{ "key": "tag", "label": "Topic" }],
"default": "[]"
}
],
"variables": [
{ "key": "accent", "label": "Accent", "type": "color", "default": "#33d6ff" },
{ "key": "plate", "label": "Plate", "type": "color", "default": "#0c0d11d9" },
{ "key": "text", "label": "Text", "type": "color", "default": "#f5f5f2" },
{ "key": "font-heading", "label": "Heading font", "type": "font", "default": "Barlow" },
{
"key": "radius",
"label": "Corner radius",
"type": "range",
"min": 0, "max": 24, "step": 1, "unit": "px",
"default": "6"
}
],
"autoOutMs": 8000,
"design": { "w": 960, "h": 200, "x": 115, "y": 783, "sizing": "fit" },
"defaultVariant": "default",
"variants": [
{ "id": "default", "name": "Default", "vars": {} },
{
"id": "light",
"name": "Light",
"vars": { "plate": "#ffffff", "text": "#16202e", "accent": "#2563eb", "radius": "0" }
}
],
"updatedAt": "2026-10-10T12:00:00Z"
}

variants/default/template.html:

<div class="strap">
<div class="plate">
<img class="photo" src="{{photo}}" alt="">
<div class="text">
<div class="name" data-fit data-fit-min="0.6">{{name}}</div>
<div class="role" data-fit data-fit-lines="2">{{role}}</div>
<ul class="tags">
<li class="tag" data-each="tags" data-limit="4">{{.tag}}</li>
</ul>
</div>
<span class="timer-bar" aria-hidden="true"></span>
</div>
</div>

variants/default/template.css:

:host {
--ease: cubic-bezier(0.2, 0, 0, 1);
--ease-in: cubic-bezier(0.4, 0, 1, 1);
}
.strap {
position: absolute;
inset: 0;
display: flex;
align-items: flex-end;
container-type: size;
}
.plate {
position: relative;
display: flex;
gap: 24px;
align-items: center;
max-width: 100%;
padding: 20px 32px 20px 20px;
background: var(--plate);
color: var(--text);
border-left: 6px solid var(--accent);
border-radius: var(--radius);
overflow: hidden;
}
.photo {
width: 120px;
height: 120px;
object-fit: cover;
border-radius: var(--radius);
}
.photo[data-empty],
.role[data-empty],
.tags[data-empty] {
display: none;
}
.text { min-width: 0; }
.name {
font: 700 56px/1.1 var(--font-heading);
white-space: nowrap;
}
.role {
margin-top: 4px;
font: 400 28px/1.25 var(--font-heading);
opacity: 0.85;
}
.tags {
display: flex;
gap: 8px;
margin: 10px 0 0;
padding: 0;
list-style: none;
}
.tag {
padding: 2px 10px;
border: 1px solid var(--accent);
border-radius: 999px;
font: 600 18px/1.4 var(--font-heading);
}
.tag[data-row="in"] {
animation: fade-up 200ms var(--ease) both;
animation-delay: calc(var(--i) * 60ms + 200ms);
}
.tag[data-row="out"] {
animation: fade-out 160ms var(--ease-in) forwards;
}
/* Drains over the time on air (autoOutMs). */
.timer-bar {
position: absolute;
left: 0;
right: 0;
bottom: 0;
height: 3px;
background: var(--accent);
transform-origin: 0 50%;
}
.strap[data-state="in"] .timer-bar {
animation: drain var(--auto-out-ms, 0ms) linear both;
}
.strap[data-state="in"] .plate {
animation: plate-in 320ms var(--ease) both;
}
.strap[data-state="out"] .plate {
animation: plate-out 220ms var(--ease-in) forwards;
}
@keyframes plate-in {
from { opacity: 0; transform: translateX(-24px); }
}
@keyframes plate-out {
to { opacity: 0; transform: translateX(-24px); }
}
@keyframes fade-up {
from { opacity: 0; transform: translateY(8px); }
}
@keyframes fade-out {
to { opacity: 0; }
}
@keyframes drain {
to { transform: scaleX(0); }
}

The light look differs only in its vars, so variants/light/ can hold a copy of the same two files.