Loading...
Loading...
The score bug is the persistent graphic in the corner of every live sports broadcast — showing teams, scores, time, and period. This tutorial covers the full lifecycle including customActions, the OGraf mechanism for triggering one-off visual events like a goal flash without changing the graphic's step.
customActions
This is the only tutorial that teaches customAction — a method for triggering visual events (goal flash, card shown) without advancing the graphic's step.
Persistent position
Unlike lower thirds that play in and out, the score bug stays on screen for the entire match. It plays in once and receives continuous updates.
Dark, compact design
Small footprint with high contrast. Dark background ensures readability over any video content — bright pitch, crowd shots, replays.
When a goal is scored, the playout system calls customAction with an action name. The graphic flashes the scoring team's side, plays a brief animation, and returns to normal — all without changing the graphic's step or requiring a full update cycle.
async customAction({ action, data }) {
if (action === 'goal') {
const side = data?.team === 'home' ? 'left' : 'right';
const scoreEl = this.querySelector(`.score-${side}`);
const flashEl = this.querySelector('.goal-flash');
// Update the score
if (data?.score !== undefined) {
scoreEl.textContent = data.score;
}
// Trigger the flash animation
flashEl.classList.add('active');
scoreEl.classList.add('pulse');
await new Promise(r => setTimeout(r, 2000));
flashEl.classList.remove('active');
scoreEl.classList.remove('pulse');
return { statusCode: 200 };
}
if (action === 'card') {
// Show yellow/red card indicator briefly
const indicator = this.querySelector('.card-indicator');
indicator.className = `card-indicator ${data?.cardType || 'yellow'}`;
indicator.classList.add('show');
await new Promise(r => setTimeout(r, 3000));
indicator.classList.remove('show');
return { statusCode: 200 };
}
return { statusCode: 404, description: `Unknown action: ${action}` };
}Key insight: customAction vs updateAction
updateAction changes the graphic's persistent data (score, time, team names). customAction triggers a transient visual event — it plays an animation, then the graphic returns to its previous visual state. Think of it as a notification overlay on top of the base graphic.
The goal flash is a full-width overlay that pulses with the scoring team's color. The score number itself also scales up briefly with a pulse class.
.score-bug {
position: absolute; /* against the graphic's root, not the viewport */
top: 32px;
left: 48px;
background: rgba(15, 15, 25, 0.92);
backdrop-filter: blur(8px);
border-radius: 8px;
display: flex;
align-items: center;
padding: 0;
overflow: hidden;
font-family: 'Inter', system-ui, sans-serif;
}
.goal-flash {
position: absolute;
inset: 0;
background: rgba(255, 215, 0, 0.3);
opacity: 0;
transition: opacity 0.15s ease;
pointer-events: none;
}
.goal-flash.active {
opacity: 1;
animation: flash-pulse 0.6s ease-in-out 3;
}
@keyframes flash-pulse {
0%, 100% { opacity: 0.3; }
50% { opacity: 0.8; }
}
.score-left.pulse,
.score-right.pulse {
animation: score-bump 0.5s cubic-bezier(0.16, 1, 0.3, 1);
}
@keyframes score-bump {
0% { transform: scale(1); }
40% { transform: scale(1.4); }
100% { transform: scale(1); }
}
.team-active {
background: rgba(255, 255, 255, 0.08);
}Design tip
Highlight the team currently in possession by adding a subtle team-active background class. This tiny detail — common in premium sports broadcasts — gives viewers a subconscious sense of momentum without being distracting.
Notice the customActions array — that's how OGraf declares graphic-specific operations beyond play/stop. The graphic must return statusCode 404 for anything not listed.
{
"$schema": "https://ograf.ebu.io/v1/specification/json-schemas/graphics/schema.json",
"id": "dev.ograf.tutorial.score-bug",
"version": "1.0.0",
"name": "Score Bug",
"description": "Persistent on-screen scoreboard with a goal customAction that flashes the bug when a team scores. Tutorial from ograf.dev.",
"author": {
"name": "ograf.dev",
"url": "https://ograf.dev"
},
"main": "graphic.mjs",
"stepCount": 1,
"supportsRealTime": true,
"supportsNonRealTime": false,
"customActions": [
{
"id": "goal",
"name": "Goal",
"description": "Flash the scoreboard to celebrate a goal.",
"schema": null
}
],
"schema": {
"type": "object",
"properties": {
"home": {
"type": "string",
"title": "Home Team",
"gddType": "single-line",
"default": "BAR"
},
"away": {
"type": "string",
"title": "Away Team",
"gddType": "single-line",
"default": "RMA"
},
"homeScore": {
"type": "integer",
"title": "Home Score",
"minimum": 0,
"default": 2
},
"awayScore": {
"type": "integer",
"title": "Away Score",
"minimum": 0,
"default": 1
},
"time": {
"type": "string",
"title": "Match Time",
"gddType": "single-line",
"default": "67:42"
},
"period": {
"type": "string",
"title": "Period",
"gddType": "single-line",
"default": "2nd Half"
}
}
}
}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.
score-bug.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
Persistent positioning, live updates via updateAction, and transient goal flashes via customAction — the full live sports toolkit.

Lower Third
Name & title overlay

Bug / LIVE
Corner indicator with pulse

News Ticker
Scrolling headline crawl

Full Page Quote
Cinematic full-screen typography

Election Bars
Animated percentage chart

Sport Lineup
Team roster grid

Countdown Timer
Self-ticking clock

Breaking News
Full-screen urgent alert

Weather Forecast
Conditions & 3-day outlook

Social Media Card
Post overlay with avatar