Loading...
Loading...
The pre-match lineup graphic is a staple of sports broadcasting — from Premier League coverage to the World Cup. A grid of player cards reveals one by one, showing name, number, and position, followed by the formation and coaching staff.
Grid layout
Uses CSS Grid to arrange player cards in responsive columns. Cards reflow naturally whether you have 11 players or 5 subs.
Staggered cards
Each player card gets an increasing transition-delay, creating a wave of cards appearing across the grid.
Two-part structure
Dark header bar with team name, then the player grid, then a footer with formation and coach — three distinct visual zones.
The _renderPlayers method creates a card for each player. The number gets a bold circle treatment, and the position badge sits beneath the name. Each card's transition-delay is calculated from its index.
_renderPlayers(players) {
const grid = this.querySelector('.player-grid');
grid.innerHTML = '';
players.forEach((player, i) => {
const card = document.createElement('div');
card.className = 'player-card';
card.style.transitionDelay = `${i * 80}ms`;
card.innerHTML = `
<div class="player-number">${player.number}</div>
<div class="player-name">${player.name}</div>
<div class="player-position">${player.position}</div>
`;
grid.appendChild(card);
});
}
async load({ data }) {
if (data?.team) this._teamName.textContent = data.team;
if (data?.meta) this._meta.textContent = data.meta;
if (data?.players) this._renderPlayers(data.players);
if (data?.formation) this._formation.textContent = data.formation;
if (data?.coach) this._coach.textContent = `Coach: ${data.coach}`;
return { statusCode: 200 };
}
async playAction() {
this._root.classList.add('visible');
await new Promise(r => setTimeout(r, 1200));
return { statusCode: 200, currentStep: 0 };
}The player grid uses grid-template-columns: repeat(auto-fill, minmax(100px, 1fr)) so it adapts to the container width. Each card fades and scales in when the parent gets the visible class.
.lineup-header {
background: #1a1a2e;
padding: 16px 24px;
display: flex;
justify-content: space-between;
align-items: center;
}
.player-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
gap: 8px;
padding: 16px;
}
.player-card {
background: rgba(255, 255, 255, 0.08);
border-radius: 8px;
padding: 12px 8px;
text-align: center;
opacity: 0;
transform: scale(0.8) translateY(10px);
transition: opacity 0.4s ease, transform 0.4s ease;
/* transition-delay set per card via JS */
}
.visible .player-card {
opacity: 1;
transform: scale(1) translateY(0);
}
.player-number {
width: 36px;
height: 36px;
border-radius: 50%;
background: rgba(255, 255, 255, 0.15);
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto 8px;
font-weight: 700;
font-size: 16px;
}
.lineup-footer {
background: #1a1a2e;
padding: 12px 24px;
display: flex;
justify-content: space-between;
font-size: 13px;
color: rgba(255, 255, 255, 0.6);
}Design tip
Use auto-fill instead of a fixed column count. This way the grid gracefully handles 11 starters, 5 substitutes, or any other roster size without layout changes in the template code.
The players field is a typed array — items.type is object with required number, name, and position. A controller can add, remove, and reorder rows automatically.
{
"$schema": "https://ograf.ebu.io/v1/specification/json-schemas/graphics/schema.json",
"id": "dev.ograf.tutorial.sport-lineup",
"version": "1.0.0",
"name": "Sport Lineup",
"description": "Team roster grid with numbered player cards and staggered reveal. 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": {
"team": {
"type": "string",
"title": "Team",
"gddType": "single-line",
"default": "FC Barcelona"
},
"meta": {
"type": "string",
"title": "Match Info",
"gddType": "single-line",
"default": "La Liga — Matchday 32"
},
"formation": {
"type": "string",
"title": "Formation",
"gddType": "single-line",
"default": "4-3-3"
},
"coach": {
"type": "string",
"title": "Coach",
"gddType": "single-line",
"default": "H. Flick"
},
"players": {
"type": "array",
"title": "Players",
"items": {
"type": "object",
"properties": {
"number": {
"type": "integer",
"title": "Number",
"minimum": 0
},
"name": {
"type": "string",
"title": "Name",
"gddType": "single-line"
},
"position": {
"type": "string",
"title": "Position",
"gddType": "single-line"
}
},
"required": [
"number",
"name",
"position"
]
},
"default": [
{
"number": 1,
"name": "ter Stegen",
"position": "GK"
},
{
"number": 23,
"name": "Koundé",
"position": "RB"
},
{
"number": 4,
"name": "Araújo",
"position": "CB"
},
{
"number": 15,
"name": "Christensen",
"position": "CB"
},
{
"number": 28,
"name": "Baldé",
"position": "LB"
},
{
"number": 8,
"name": "Pedri",
"position": "CM"
},
{
"number": 16,
"name": "Fermín",
"position": "CM"
},
{
"number": 6,
"name": "Gavi",
"position": "CM"
},
{
"number": 11,
"name": "Yamal",
"position": "RW"
},
{
"number": 9,
"name": "Lewandowski",
"position": "ST"
},
{
"number": 19,
"name": "Raphinha",
"position": "LW"
}
]
}
}
}
}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.
sport-lineup.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
CSS Grid, staggered card reveals, and a clean header/footer structure — ready for matchday broadcasts.

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

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