BeatOff API
An embeddable A/B elimination bracket widget. Display two content cards side by side, let users pick a winner, replace the loser with a new challenger. The loop never ends.
What you can build
- Music battles - Let users pick between AI-generated tracks (uses BeatKids for live playback)
- Quote showdowns - Bible verses, fortune cookies, inspirational garbage
- Image face-offs - Memes, art, photos - let the people decide
- Custom content - Register your own adapter for any content type
- Endless engagement - Users will beat off for hours. Literally hours.
Live Demo
See what you can build with the BeatOff API.
Quickstart
Get up and running with BeatOff in under a minute.
1. Add the widget container
<div id="beatoff-widget"
data-source="beatkids-music"
data-content-type="audio"
data-show-leaderboard="true"
data-show-stats="true">
</div>
2. Include the scripts
<!-- BeatKids core for music playback (only needed for audio content) --> <script src="https://beatkids.vagibond.com/beatkids-core.js"></script> <!-- BeatOff widget --> <script src="https://beatkids.vagibond.com/beatoff-core.js" async></script>
That's it! The widget auto-initializes when the page loads.
Configuration
Configure the widget via data attributes or the JavaScript API:
| Attribute | Type | Default | Description |
|---|---|---|---|
data-source | string | 'beatkids-music' | Content source identifier from beatoff.json |
data-content-type | string | 'audio' | Content type: 'audio', 'text', 'image', or custom |
data-data-url | string | '/data/beatoff.json' | URL to fetch content data from |
data-show-leaderboard | boolean | true | Show the fake leaderboard |
data-show-stats | boolean | true | Show session stats (time wasted, votes) |
data-bg-color | color | '#0a0a0f' | Widget background color |
data-primary-color | color | '#ff00ff' | Primary accent color |
data-accent-color | color | '#00ffff' | Secondary accent color |
init(options)
Manually initialize widgets with custom options:
BeatOff.init({
source: 'beatkids-music',
contentType: 'audio',
dataUrl: '/data/beatoff.json',
primaryColor: '#ff00ff',
onVote: function(winner, loser) {
console.log('User picked:', winner.title, 'over', loser.title);
}
});
registerAdapter(type, adapter)
Register a custom content adapter for any content type:
BeatOff.registerAdapter('grunt', { render: function(item, container, config, side) { // Render the card HTML container.innerHTML = ` <div class="beatoff-card" data-side="${side}"> <div class="grunt-text">${item.grunt}</div> <button class="beatoff-vote-btn">This One</button> </div> `; // Set up vote handler container.querySelector('.beatoff-vote-btn').onclick = () => { window.onBeatOffVote(item, side); }; }, play: function(item) { // Optional: play audio/video Gruntslate.speak(item.grunt); }, stop: function() { // Optional: stop playback Gruntslate.stopSpeaking(); } });
getStats()
Get current session statistics:
const stats = BeatOff.getStats(); console.log(stats.votes); // 42 console.log(stats.timeWasted); // "47m 23s" console.log(stats.topWinner); // "bo-003"
getLeaderboard()
Get the user's voting history (merged with fake leaderboard):
const leaderboard = BeatOff.getLeaderboard(); // { "bo-003": 5, "bo-007": 3, ... }
Audio Content Type
The audio adapter uses BeatKids for live music playback. Each item needs:
{
"id": "bo-001",
"title": "Dumpster Serenade",
"artist": "DJ Boxcar",
"style": "lofi",
"seed": "dumpster-serenade"
}
Text Content Type
For quotes, verses, or any text content:
{
"id": "quote-001",
"text": "Life is like a box of chocolates...",
"author": "Forrest Gump"
}
Image Content Type
For image face-offs:
{
"id": "img-001",
"url": "https://example.com/image.jpg",
"title": "A beautiful sunset"
}
Data Structure
The beatoff.json file structure:
{
"sources": {
"my-source": {
"type": "audio",
"label": "My BeatOff",
"description": "Pick your favorite...",
"items": [ ... ]
}
},
"leaderboard": {
"my-source": [
{ "title": "Top Track", "wins": 14847 },
...
]
}
}