Loading...
Loading...
Election bars are the backbone of results night coverage — think CNN's election night or the BBC's general election broadcast. Each row represents a party with a color-coded bar that grows to its vote percentage, creating a visceral, at-a-glance comparison of results.
Data-driven widths
Bar widths are set via style.width = percent + '%' inline — no fixed CSS classes. The data controls the visual.
Staggered reveal
Each row animates in with increasing transition-delay, creating a cascading waterfall effect from top to bottom.
Color-coded identity
Each party's bar color is passed in the data — no hardcoded palette. Works for any election, any country, any party system.
The _renderBars method loops through each party, creates a row with the party name, a colored bar sized to its percentage, and a vote count. The key trick: bar width starts at 0 and transitions to its target percentage when the visible class is added.
_renderBars(parties) {
const container = this.querySelector('.bars-container');
container.innerHTML = '';
parties.forEach((party, i) => {
const row = document.createElement('div');
row.className = 'bar-row';
row.style.transitionDelay = `${i * 120}ms`;
const bar = document.createElement('div');
bar.className = 'bar-fill';
bar.style.backgroundColor = party.color;
bar.style.width = '0%';
const label = document.createElement('span');
label.className = 'bar-label';
label.textContent = party.name;
const value = document.createElement('span');
value.className = 'bar-value';
value.textContent = party.percent > 15
? `${party.percent}%`
: '';
const votes = document.createElement('span');
votes.className = 'bar-votes';
votes.textContent = party.votes.toLocaleString();
bar.appendChild(value);
row.append(label, bar, votes);
container.appendChild(row);
// Store target width for animation
bar._targetWidth = party.percent + '%';
});
}
async playAction() {
// Animate bars to their target widths
this.querySelectorAll('.bar-fill').forEach(bar => {
bar.style.width = bar._targetWidth;
});
this._root.classList.add('visible');
await new Promise(r => setTimeout(r, 800));
return { statusCode: 200, currentStep: 0 };
}The bar fill uses a CSS transition on width. Combined with the staggered transition-delay on each row, you get the classic election night cascading reveal.
.bar-row {
display: flex;
align-items: center;
gap: 12px;
opacity: 0;
transform: translateX(-20px);
transition: opacity 0.5s ease, transform 0.5s ease;
/* transition-delay set per row via JS */
}
.visible .bar-row {
opacity: 1;
transform: translateX(0);
}
.bar-fill {
height: 36px;
border-radius: 4px;
width: 0%;
transition: width 1s cubic-bezier(0.16, 1, 0.3, 1);
display: flex;
align-items: center;
justify-content: flex-end;
padding-right: 8px;
}
.bar-label {
width: 160px;
font-weight: 600;
font-size: 14px;
text-align: right;
flex-shrink: 0;
}
.bar-votes {
font-size: 13px;
color: rgba(255, 255, 255, 0.6);
min-width: 90px;
}Design tip
Use asymmetric percentage labels — show the percentage inside the bar when it's wider than 15%, and outside (or omit it) when the bar is small. This prevents tiny bars from overflowing their label, a common pitfall in election graphics.
Large vote counts are hard to read without separators. Use toLocaleString() to automatically format numbers with thousands separators based on the viewer's locale — 13,966,454 is far more readable than 13966454.
// Format votes with locale-aware thousands separators
votes.textContent = party.votes.toLocaleString();
// en-US: "13,966,454"
// de-DE: "13.966.454"
// fr-FR: "13 966 454"The parties field is a typed array — items.type is object with required name, pct, and color. That's how OGraf declares structured repeating data.
{
"$schema": "https://ograf.ebu.io/v1/specification/json-schemas/graphics/schema.json",
"id": "dev.ograf.tutorial.election-bars",
"version": "1.0.0",
"name": "Election Bars",
"description": "Animated horizontal bar chart for election results with data-driven widths and color-coded parties. Tutorial from ograf.dev.",
"author": {
"name": "ograf.dev",
"url": "https://ograf.dev"
},
"main": "graphic.mjs",
"stepCount": 1,
"supportsRealTime": true,
"supportsNonRealTime": false,
"schema": {
"type": "object",
"properties": {
"title": {
"type": "string",
"title": "Title",
"gddType": "single-line",
"default": "General Election 2026"
},
"subtitle": {
"type": "string",
"title": "Subtitle",
"gddType": "single-line",
"default": "78% reporting"
},
"parties": {
"type": "array",
"title": "Parties",
"items": {
"type": "object",
"properties": {
"name": {
"type": "string",
"title": "Name",
"gddType": "single-line"
},
"pct": {
"type": "number",
"title": "Percentage",
"minimum": 0,
"maximum": 100
},
"votes": {
"type": "integer",
"title": "Votes",
"minimum": 0
},
"color": {
"type": "string",
"title": "Color",
"gddType": "color-rrggbb",
"pattern": "^#[0-9a-f]{6}quot;
}
},
"required": [
"name",
"pct",
"color"
]
},
"default": [
{
"name": "Progressives",
"pct": 42,
"votes": 1284000,
"color": "#2563eb"
},
{
"name": "Conservatives",
"pct": 35,
"votes": 1070000,
"color": "#dc2626"
},
{
"name": "Centrists",
"pct": 15,
"votes": 459000,
"color": "#f59e0b"
},
{
"name": "Greens",
"pct": 8,
"votes": 245000,
"color": "#16a34a"
}
]
}
}
}
}A real OGraf Graphics Definition v1 package. A compliant renderer reads the manifest and drives the lifecycle. MIT-licensed; drop it into any OGraf-compatible system.
election-bars.ograf.json
Manifest — what a renderer reads (id, schema, lifecycle flags)
graphic.mjs
Web Component with load / play / update / stop / customAction / dispose
style.css
Stylesheet, loaded by graphic.mjs via a <link> tag
README.md
Usage notes
Data-driven widths, staggered reveals, and color-coded parties — everything you need for results night coverage.

Lower Third
Name & title overlay

Bug / LIVE
Corner indicator with pulse

News Ticker
Scrolling headline crawl

Full Page Quote
Cinematic full-screen typography

Sport Lineup
Team roster grid

Score Bug
Live match scoreboard

Countdown Timer
Self-ticking clock

Breaking News
Full-screen urgent alert

Weather Forecast
Conditions & 3-day outlook

Social Media Card
Post overlay with avatar