Docs / Info Display
How to Make a Custom Info Display Theme
The Info Display window is the audience-facing screen for the currently playing song, tanda, or cortina. You can customize it with plain HTML, CSS, images, and a small JavaScript API.
What a Theme Is
An Info Display theme is a folder inside TangoDJ's info display themes folder. TangoDJ reads the files in that folder and uses them to render the separate Info Display window.
A theme can be simple: four HTML fragments and one CSS file. It can also include images, fonts, video, and JavaScript for dynamic layout or animation.
Start From a Copy
The safest workflow is to copy one of the built-in theme folders, rename the copy, then edit the copy. Do not edit the built-in folder directly if you want to keep your changes separate from TangoDJ's bundled examples.
Built-in themes currently include: current_song, current_next_song, current_tanda, current_next_tanda, flash_spectacle, and js_demo.
display/
current_song/
current_next_song/
js_demo/
my_theme/
park.html
track.html
tanda.html
cortina.html
style.css
index.js optional
js_libraries optional
Template Files
TangoDJ chooses one body template each time it renders the display:
- park.html: shown when nothing is currently playing.
- track.html: shown for a normal track inside the current tanda.
- tanda.html: shown when the current track is the first track of the current tanda.
- cortina.html: shown when the current track is marked as a cortina.
- style.css: loaded into the Info Display window for the selected theme.
The HTML files are fragments, not full HTML pages. TangoDJ provides the outer page and places your fragment into the full-screen display area.
<main class="theme-screen theme-track">
<p class="eyebrow">Now Playing</p>
<h1 class="title">{{ current_track.title }}</h1>
<p class="meta">{{ current_track.artist }}</p>
<p class="meta">{{ current_track.singer }}</p>
</main>
Placeholders
Use double curly braces to insert track and tanda data. Placeholder values are HTML-escaped before they are inserted, so they are safe to display as text.
Available top-level objects are: prev_track, current_track, next_track, prev_tanda, current_tanda, and next_tanda.
{{ current_track.title }}
{{ current_track.artist }}
{{ current_track.genre }}
{{ current_tanda.title }}
{{ current_tanda.current_track_number }}
{{ current_tanda.number_of_tracks }}
{{ next_track.title }}
{{ next_tanda.artist }}
Placeholder lookup accepts snake_case and camelCase variants. It also checks a track's tag object, so a field stored as metadata can still be displayed if it is available in the track data.
Assets
Put theme assets in the same folder as your HTML and CSS. Reference them with the display-asset:// scheme. TangoDJ rewrites that scheme to a safe local asset URL for the selected theme.
<img
class="orchestra-photo"
src="display-asset://photo_disarli.jpg"
alt="Di Sarli" />
.theme-screen {
background-image:
linear-gradient(rgba(0, 0, 0, 0.25), rgba(0, 0, 0, 0.7)),
url("display-asset://background.jpg");
}
In JavaScript, use TangoDJInfoDisplay.getAssetUrl() or the event's getAssetUrl() helper instead of building local file paths by hand.
Using JavaScript
Add an index.js file to the theme folder when you need behavior after each render: canvas animation, fitting long text, choosing an image based on the orchestra, or updating data attributes.
Theme scripts run in the Info Display window and use the global TangoDJInfoDisplay API. Register an onRender handler to receive the rendered root element, display mode, selected theme id, blackout state, and cloned template context.
function text(value) {
return value === undefined || value === null ? "" : String(value).trim();
}
function render(event) {
var title = event.root.querySelector("[data-title]");
var currentTrack = event.context.current_track || {};
var currentTanda = event.context.current_tanda || {};
if (title) {
title.textContent = text(currentTrack.title) || text(currentTanda.title);
}
event.root.dataset.mode = event.mode;
event.root.dataset.hasNext = event.context.next_track ? "true" : "false";
}
TangoDJInfoDisplay.onRender(render);
If your script starts an animation, timer, media object, or third-party widget, also register onCleanup. TangoDJ calls cleanup before loading another theme script.
var frameId = null;
function startAnimation() {
function tick() {
frameId = requestAnimationFrame(tick);
}
tick();
}
TangoDJInfoDisplay.onRender(startAnimation);
TangoDJInfoDisplay.onCleanup(function () {
if (frameId !== null) {
cancelAnimationFrame(frameId);
frameId = null;
}
});
Loading Extra JavaScript Files
If index.js depends on helper scripts, create a js_libraries file. Add one relative file path per line. Empty lines and lines starting with # are ignored. TangoDJ loads these files before index.js.
# js_libraries
wave-background.js
text-fit.js
Test While Editing
- Open the Info Display window from Info Display > Open/Close.
- Hover over the top-right corner of the display window and choose your theme from the selector.
- Click Watch so TangoDJ reloads the display after file changes.
- Play a normal track, a first track in a tanda, and a cortina so you see track, tanda, and cortina modes.
- Resize the display window or move it to the second output to check text wrapping and image scaling.
The display window also exposes useful CSS variables such as --display-stage-scale, --display-type-scale, and --display-space-scale. Use them to keep the same theme readable on a laptop preview window and a projector.
Starter Theme
This minimal theme gives you a full-screen layout, placeholders for the active music, and a JavaScript hook that marks the current display mode.
track.html
<main class="starter-screen" data-starter-root>
<p class="starter-eyebrow">Now Playing</p>
<h1 class="starter-title">{{ current_track.title }}</h1>
<p class="starter-meta">{{ current_track.artist }}</p>
<p class="starter-meta">{{ current_track.singer }}</p>
<p class="starter-chip">{{ current_track.genre }}</p>
</main>
style.css
.starter-screen {
box-sizing: border-box;
min-height: 100vh;
display: grid;
align-content: center;
gap: clamp(12px, calc(18px * var(--display-space-scale, 1)), 28px);
padding: clamp(32px, calc(72px * var(--display-space-scale, 1)), 96px);
color: #fff7ed;
background:
radial-gradient(circle at var(--display-spotlight-x, 30%) 20%,
var(--display-primary-soft, rgba(255, 120, 80, 0.25)),
transparent 38%),
linear-gradient(135deg, #140b08, #050505);
font-family: "Helvetica Neue", Arial, sans-serif;
}
.starter-eyebrow,
.starter-chip {
margin: 0;
text-transform: uppercase;
letter-spacing: 0.18em;
opacity: 0.72;
}
.starter-title {
margin: 0;
max-width: var(--display-headline-measure, 12ch);
font-size: clamp(44px, calc(96px * var(--display-type-scale, 1)), 124px);
line-height: var(--display-headline-line-height, 0.95);
}
.starter-meta {
margin: 0;
font-size: clamp(22px, calc(34px * var(--display-type-scale, 1)), 44px);
opacity: 0.86;
}
index.js
TangoDJInfoDisplay.onRender(function (event) {
var root = event.root.querySelector("[data-starter-root]");
if (!root) {
return;
}
root.dataset.mode = event.mode;
root.dataset.blackout = event.blackout ? "true" : "false";
});
Duplicate that structure into park.html, tanda.html, and cortina.html, then change the copy and placeholders for each mode.