Loading...
Loading...
A corner bug is a small branded element — LIVE indicator, channel logo, event badge — that sits in a corner of the screen. It pops in with a scale animation and fades out cleanly.
Position
Top-right corner. position: absolute; top: 40px; right: 40px — never fixed, which would escape to the viewport instead of the 1920×1080 render area.
Animation
Scale from 50% + blur instead of slide. A "materializing" effect — less intrusive than a slide for something that stays on screen.
Pulsing LIVE dot
A CSS @keyframes pulse on an absolutely-positioned sibling creates the broadcast-style LIVE ping without any JavaScript.
{
"$schema": "https://ograf.ebu.io/v1/specification/json-schemas/graphics/schema.json",
"id": "dev.ograf.tutorial.bug",
"version": "1.0.0",
"name": "Corner Bug / LIVE",
"description": "Corner indicator that pops in with a scale animation and a pulsing LIVE dot. 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": {
"label": {
"type": "string",
"title": "Label",
"gddType": "single-line",
"default": "LIVE"
},
"sublabel": {
"type": "string",
"title": "Sublabel",
"gddType": "single-line",
"default": "Breaking News"
}
}
}
}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.
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
Instead of sliding in, the bug scales up from 50% with a blur. This creates a subtle "materializing" effect that's less intrusive than a slide — perfect for something that sits in the corner.
.bug {
position: absolute; /* Anchors to the renderer's frame, not the viewport */
top: 40px;
right: 40px;
transform: scale(0.5); /* Start small */
opacity: 0;
filter: blur(8px); /* Start blurred */
}
.bug.visible {
transform: scale(1); /* Scale to full size */
opacity: 1;
filter: blur(0); /* Sharpen */
transition: transform 0.6s cubic-bezier(0.16, 1, 0.3, 1),
opacity 0.4s ease,
filter 0.4s ease;
}
.bug.out {
transform: scale(0.8); /* Shrink slightly on exit */
opacity: 0;
filter: blur(8px);
}Design tip
The out-animation shrinks to 80% (not 50%) and uses a faster easing. This asymmetry — slow in, quick out — feels natural. The eye notices the entrance but barely registers the exit.
Same shape as the lower third: a <link> to the stylesheet (absolute URL via import.meta.url), a lazy _initDom(), and all six lifecycle methods. No module-level customElements.define() — the renderer picks the tag.
const STYLE_URL = new URL('./style.css', import.meta.url).href;
const TEMPLATE = `
<link rel="stylesheet" href="${STYLE_URL}">
<div class="bug">
<div class="bug-container">
<div class="bug-live">
<div class="bug-live-ping"></div>
<div class="bug-live-dot"></div>
</div>
<div class="bug-text">
<div class="bug-label"></div>
<div class="bug-sublabel"></div>
</div>
</div>
</div>
`;
export default class BugGraphic extends HTMLElement {
_initDom() {
if (this._initialized) return;
this.innerHTML = TEMPLATE;
this._root = this.querySelector('.bug');
this._label = this.querySelector('.bug-label');
this._sublabel = this.querySelector('.bug-sublabel');
this._initialized = true;
}
async load({ data } = {}) {
this._initDom();
if (data?.label) this._label.textContent = data.label;
if (data?.sublabel) this._sublabel.textContent = data.sublabel;
return { statusCode: 200 };
}
async playAction({ skipAnimation } = {}) {
this._initDom();
this._root.classList.remove('out');
if (skipAnimation) {
this._root.classList.add('visible');
return { statusCode: 200, currentStep: 0 };
}
void this._root.offsetWidth;
this._root.classList.add('visible');
await new Promise(r => setTimeout(r, 600));
return { statusCode: 200, currentStep: 0 };
}
async stopAction({ skipAnimation } = {}) {
this._initDom();
if (skipAnimation) {
this._root.classList.remove('visible');
return { statusCode: 200 };
}
this._root.classList.add('out');
await new Promise(r => setTimeout(r, 400));
this._root.classList.remove('visible', 'out');
return { statusCode: 200 };
}
async updateAction({ data } = {}) {
this._initDom();
if (data?.label) this._label.textContent = data.label;
if (data?.sublabel) this._sublabel.textContent = data.sublabel;
return { statusCode: 200 };
}
async customAction({ action } = {}) {
return { statusCode: 404, description: `Unknown custom action: ${action ?? ""}` };
}
async dispose() {
this.innerHTML = '';
this._initialized = false;
return { statusCode: 200 };
}
}Same OGraf package pattern — manifest, CSS, Web Component. Different visual, same interoperability.

Lower Third
Name & title overlay

News Ticker
Scrolling headline crawl

Full Page Quote
Cinematic full-screen typography

Election Bars
Animated percentage chart

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