PitLane Systems

Custom Overlays

Building your own overlays, the manifest, live data, controls, families, and packaging as .pitlane packs.

PitLane's overlay system is built to be extended. You can create overlays with plain HTML, CSS, and JavaScript, and PitLane will serve them, feed them live race data, and list them in the Library beside the built-in set. When you are ready to share, you can package overlays as a .pitlane file that anyone can install with a double-click.

How overlays work

Every overlay is a web page served by PitLane's overlay server on localhost:9100. It receives live race data over a WebSocket and renders it however it wants. OBS shows it as a browser source.

The five built-in template overlays (leaderboard, lower-third, battle, session-info, ticker) are each one HTML file in its own folder with a manifest beside it, written to be read and copied. A bare .html file with an embedded manifest works too. Start from one of them.

Creating a basic overlay

1. Create a folder

Make a folder named after your overlay's id, with an index.html and a manifest.json inside.

your-overlay/
  index.html
  manifest.json

While developing, put the folder under PitLane's user overlays directory, one level inside a grouping folder of your choice

%APPDATA%\pitlane\overlays\my-overlays\your-overlay\

Do not put files inside the install directory. An app update replaces everything there. For sharing with others, package your work as a pack. See Packaging as a .pitlane pack.

2. Write the manifest

{
  "id": "your-overlay",
  "name": "My Custom Overlay",
  "author": "Your Name",
  "version": "1.0",
  "description": "A brief description of what this overlay shows.",
  "tags": ["community"],
  "icon": "Tv",
  "type": "custom",
  "controls": []
}

Required fields

  • id. Unique identifier. Must match the folder name.
  • name. Display name shown in the Library and the Overlays panel.
  • controls. Array of control definitions. Can be empty.

Optional fields

  • author, version, description. Shown in the Library's detail panel.
  • tags. Categorization such as "community".
  • icon. A Lucide icon name used when there is no preview image. Available icons are List, LayoutList, User, UserRound, Swords, Info, MessageSquare, Timer, Gauge, Flag, Trophy, Radio, Tv, BarChart3, Map, Layers, Zap, Eye, Activity, and Clock.
  • type. What kind of overlay this is. One of "timing-tower", "lower-third", "battle", "telemetry", "track-map", "ticker", "session-info", "event-board", "sponsor", or "custom". The type names the overlay in the Library, routes it to /slot/<type> serving, and lets family switching keep one overlay of each kind on screen.
  • family. The id of an overlay family this overlay belongs to.
  • preview. A path to a cover image inside the overlay folder, such as "preview.png". Shown on the overlay's Library card. Cards fall back to the icon when absent.
  • previewQuery. Extra query parameters for the Library's live example, such as "demo-board=intro". Use it when your overlay needs a specific trigger to show something in a preview.

3. Connect to live data

<!DOCTYPE html>
<html>
<head>
  <style>
    body {
      margin: 0;
      background: transparent;
      font-family: sans-serif;
      color: white;
    }
  </style>
</head>
<body>
  <div id="overlay"></div>
  <script>
    const ws = new WebSocket('ws://localhost:9100/live');

    ws.onmessage = (event) => {
      const data = JSON.parse(event.data);
      // data.cars is every car
      // data.focusedCar is the currently spectated car
      // data.battles is the active battles
      // data.events is recent race events
      // data.session is session info such as weather, flags, and time
      // data.overlayControls['your-overlay'] is your control values
      render(data);
    };

    function render(data) {
      // Your rendering logic here
    }
  </script>
</body>
</html>

Keep background: transparent so your overlay layers cleanly over the game capture in OBS.

4. Add it in PitLane

  1. Open the Overlay Library (the + button in the Overlays panel, or View > Workspaces > Overlay Library)
  2. Your overlay appears with the built-ins. The Library re-scans when you open it, so a freshly dropped folder shows up without a restart.
  3. Click Add to Broadcast on its card
  4. Show or hide it from the Overlays panel like any other overlay

5. View it in OBS

If you use the single broadcast layer from the scene wizard, your overlay is already included whenever it is shown. For a direct source, point a browser source at http://localhost:9100/overlay/your-overlay.

Adding controls

Controls let users configure your overlay from PitLane without editing code. Add them to the controls array in the manifest.

Control types

Toggle is an on/off switch

{
  "id": "show-gaps",
  "type": "toggle",
  "label": "Show Gaps",
  "placement": "inline",
  "default": true
}

Select is a dropdown

{
  "id": "sort-mode",
  "type": "select",
  "label": "Sort By",
  "placement": "inline",
  "options": ["position", "gap", "last-lap"],
  "default": "position"
}

Range is a numeric slider

{
  "id": "font-size",
  "type": "range",
  "label": "Font Size",
  "placement": "settings",
  "min": 12,
  "max": 32,
  "step": 2,
  "default": 16
}

Color is a color picker

{
  "id": "accent-color",
  "type": "color",
  "label": "Accent Color",
  "placement": "settings",
  "default": "#0EA5E9"
}

Text is a text input

{
  "id": "header-text",
  "type": "text",
  "label": "Header Text",
  "placement": "settings",
  "default": "Race Standings",
  "maxLength": 50,
  "placeholder": "Enter header text"
}

Button triggers a momentary action

{
  "id": "reset-animation",
  "type": "button",
  "label": "Reset Animation",
  "placement": "inline",
  "duration": 1000
}

Placement

  • "inline" shows in the overlay's expanded controls in the panel
  • "settings" shows in the overlay's settings dialog behind the gear icon

Reading control values

ws.onmessage = (event) => {
  const data = JSON.parse(event.data);
  const controls = data.overlayControls['your-overlay'] || {};
  const showGaps = controls['show-gaps'] ?? true;
  const sortMode = controls['sort-mode'] ?? 'position';
};

WebSocket data reference

Every message includes

FieldTypeDescription
carsarrayAll cars with position, lap times, speed, pit status, driver info, and more
focusedCarobject or nullDetailed telemetry for the spectated car, such as gear, pedals, RPM, and fuel
battlesarrayActive battles with the two cars involved and the gap
eventsarrayRecent race events, such as overtakes, incidents, pit stops, and fastest laps
sessionobject or nullSession info, such as weather, flags, time remaining, and track length
overlayVisibilityobjectWhether each overlay is toggled on or off
overlayControlsobjectCurrent control values per overlay, keyed by overlay id
palettesobjectEffective family palettes, keyed by family id, then slot id
slotsobjectWhich overlay currently occupies each typed slot
broadcastOverlaysarrayVisible overlay ids, in order, for the composite page
timestampnumberCurrent timestamp

Overlay families

If you are building several overlays that share a look, group them into a family. A family gives your overlays one named color palette that users edit in File > Settings... > Overlays, plus one-click activation in the Overlays panel.

1. Declare the family

For a folder-based overlay set, add a JSON file to the _families folder inside the overlays directory, named after the family id. In a pack, this file is simply family.json at the pack root.

{
  "id": "your-family",
  "name": "Your Family",
  "author": "Your Name",
  "version": "1.0",
  "palette": [
    { "id": "accent", "label": "Series Orange", "default": "#FF8000" },
    { "id": "surface", "label": "Panel", "default": "#18181B" },
    { "id": "text", "label": "Text", "default": "#FAFAFA" },
    { "id": "positive", "label": "Gaining", "default": "#22C55E" },
    { "id": "negative", "label": "Losing", "default": "#EF4444" }
  ]
}

Each palette slot becomes one color picker, labeled with your label. The five slots above (accent, surface, text, positive, negative) are the shared vocabulary every family should map. Add extra slots as needed. Only user changes are stored, and your default values are the reset target.

2. Join the family from each overlay

{
  "id": "your-timing-tower",
  "name": "Your Timing Tower",
  "family": "your-family",
  "type": "timing-tower",
  "controls": []
}

3. Use the palette

Palette values arrive in every payload under palettes["your-family"]. Apply them as CSS variables.

ws.onmessage = (event) => {
  const data = JSON.parse(event.data);
  const palette = data.palettes?.['your-family'];
  if (palette) {
    for (const [slot, hex] of Object.entries(palette)) {
      document.documentElement.style.setProperty(`--pl-${slot}`, hex);
    }
  }
  render(data);
};
.position-bar { background: var(--pl-accent, #FF8000); }

Keep the fallback in var() so the overlay looks right before the first payload arrives. Palette edits in Settings apply live, and OBS browser sources recolor without a refresh.

Packaging as a .pitlane pack

A pack is a zip file renamed to .pitlane. It holds a pack.json, one folder per overlay, and optionally a family.json. Users install it with a double-click or from the Library's Install button.

your-pack.pitlane (a zip)
  pack.json
  family.json            (optional)
  your-timing-tower/
    manifest.json
    index.html
    assets/...
  your-lower-third/
    manifest.json
    index.html

pack.json looks like this.

{
  "manifestVersion": 1,
  "apiVersion": 1,
  "packId": "your-pack",
  "name": "Your Broadcast Pack",
  "version": "1.0.0",
  "author": "Your Name",
  "description": "A short description of the pack.",
  "overlays": ["your-timing-tower", "your-lower-third"],
  "family": "your-family"
}
  • packId is the pack's stable identity. Installing the same or a newer version replaces what is there. Installing an older version is refused.
  • name, version, and author are required.
  • packId and every overlay id must be lowercase letters, numbers, and dashes.
  • overlays lists the overlay folder names inside the pack. Folder name equals overlay id.
  • family is present only when the pack ships a family.json.
  • publishedAt is an optional ISO date, shown in the Library.
  • manifestVersion and apiVersion are compatibility numbers. Use 1 for both. The app rejects packs built for a newer format than it understands, and tells the user to update PitLane.

Ship overlays pre-built. A pack contains plain HTML, JavaScript, and CSS that runs as-is. If you build with a framework, put the build output in the pack, not the source.

To test, zip the contents (not a wrapping folder), rename to .pitlane, and double-click it. Overlay ids that collide with built-in overlays are rejected, so prefix your ids with something unique to you.

Development workflow

PitLane's overlay server reads overlay files from disk with no caching. For a plain HTML overlay, edit the file and refresh the browser source. Open http://localhost:9100/overlay/your-overlay in a normal browser while developing. Add ?preview=1 to load an overlay that is not in your broadcast yet. If you want it to ignore the hide toggle too, check for that parameter in your own code the way the templates do. Add &demo=1 if your overlay supports a demo mode.

If you are developing inside the PitLane source tree as a React overlay, npm run dev:overlay -- your-overlay runs a hot-reload dev server on port 5174 that proxies to the overlay server.

Starting from a template

The five template overlays are designed to be forked. Copy one to a new folder, change the manifest, and start modifying. The battle template also includes a commented demo mode showing how to make an event-driven overlay previewable.