One script tag per widget
Each widget ships as its own NPM package under `@gomagaming/*`. Install only what you need, drop the element into any page — React, Vue, Angular, or vanilla HTML.
A sports feed UI distributed as Web Components — embed with one import and a config object.
Six widgets ship today. Each is its own NPM package; consumers install only what they need. Every widget shares the same peer-dependency set (vue, pinia, vue-router, vue-i18n, @vueuse/core) so a host that mounts more than one only ever loads one copy of the framework.
| Widget | Package | Reference |
|---|---|---|
<goma-events-horizontal> | @gomagaming/events-horizontal | API docs |
<goma-sports-navigation-horizontal> | @gomagaming/sports-navigation-horizontal | API docs |
<goma-sports-navigation-dialog> | @gomagaming/sports-navigation-dialog | API docs |
<goma-betslip-floating> | @gomagaming/betslip-floating | API docs |
<goma-betslip-sidebar> | @gomagaming/betslip-sidebar | API docs |
<goma-game-details> | @gomagaming/game-details | API docs |
<goma-events-horizontal> Swipeable row of event cards — live scores, odds buttons, and market navigation. Subscribe to a WAMP topic, or feed events straight in via the events property.
pnpm add @gomagaming/events-horizontal vue pinia vue-router vue-i18n @vueuse/core<goma-events-horizontal id="eh"></goma-events-horizontal>
<script type="module">
import '@gomagaming/events-horizontal'
const el = document.getElementById('eh')
el.config = {
socketUrl: 'wss://sportsapi.example.com/v2',
socketRealm: 'www.example.com',
apiBaseUrl: 'https://api.example.com',
bettingApiBaseUrl: 'https://sports-api.example.com',
ucsOperatorId: '4313',
locale: 'en',
}
el.topic = 'custom-matches-aggregator/1/all/all/all/POPULAR/LIVE/10/5'
el.addEventListener('goma:outcome-select', (e) => {
console.log('Betslip add:', e.detail.outcomeName, '@', e.detail.odds)
})
</script><goma-sports-navigation-horizontal> Horizontal swipeable strip of sport tiles with a live-event count badge. Three data sources: host-fed (prop), one-shot WAMP RPC (rpc), or live subscription (subscribe).
pnpm add @gomagaming/sports-navigation-horizontal vue pinia vue-router vue-i18n @vueuse/core<goma-sports-navigation-horizontal id="nav"></goma-sports-navigation-horizontal>
<script type="module">
import '@gomagaming/sports-navigation-horizontal'
const el = document.getElementById('nav')
el.config = { iconBaseUrl: '/icons', locale: 'en' }
el.dataSource = 'prop'
el.sports = [
{ id: '1', name: 'Football', iconId: 1, numberOfLiveEvents: 12 },
{ id: '3', name: 'Tennis', iconId: 3, numberOfLiveEvents: 64 },
{ id: '6', name: 'Baseball', iconId: 6, numberOfLiveEvents: 0 },
]
el.addEventListener('goma:sport-select', (e) => {
console.log('Sport selected:', e.detail.sportName, e.detail.sportId)
})
</script><goma-sports-navigation-dialog> Sister widget — same data sources, same goma:sport-select event, rendered as a trigger pill that opens a modal grid (bottom-sheet on mobile). Drop-in interchangeable with the horizontal sibling wherever a single-selection picker is needed.
pnpm add @gomagaming/sports-navigation-dialog vue pinia vue-router vue-i18n @vueuse/core<goma-sports-navigation-dialog id="picker"></goma-sports-navigation-dialog>
<script type="module">
import '@gomagaming/sports-navigation-dialog'
const el = document.getElementById('picker')
el.config = {
iconBaseUrl: '/icons',
locale: 'en',
// The pill should sit on the host's surface — keep the host transparent.
theme: { backgroundPrimary: 'transparent' },
}
el.dataSource = 'subscribe'
el.liveStatus = 'LIVE'
el.addEventListener('goma:sport-select', (e) => {
console.log('Sport selected:', e.detail.sportName)
})
</script><goma-betslip-floating> Floating-action-button betslip: fixed pill with a selection-count badge that opens a full-screen <dialog> containing the shared betslip panel. Reads selections from the cross-widget goma:betslip:v3 storage, so any <goma-events-horizontal> on the page (or another tab on the same origin) feeds it.
pnpm add @gomagaming/betslip-floating vue pinia vue-router vue-i18n @vueuse/core<goma-betslip-floating id="bf"></goma-betslip-floating>
<script type="module">
import '@gomagaming/betslip-floating'
const el = document.getElementById('bf')
el.placeBetMode = 'event'
el.addEventListener('goma:place-bet-request', async (e) => {
await fetch('/my/internal/bets', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(e.detail),
})
})
el.addEventListener('goma:bet-placed', (e) => console.log('receipt', e.detail))
</script><goma-betslip-sidebar> Always-visible vertical betslip rendered inline as an <aside>. Same shared-storage selection sync and place-bet contract as the floating sibling — use this one for desktop layouts with a permanent betslip column.
pnpm add @gomagaming/betslip-sidebar vue pinia vue-router vue-i18n @vueuse/core<div style="height: 600px; max-width: 380px;">
<goma-betslip-sidebar id="bs"></goma-betslip-sidebar>
</div>
<script type="module">
import '@gomagaming/betslip-sidebar'
const el = document.getElementById('bs')
el.placeBetMode = 'event'
el.addEventListener('goma:place-bet-request', async (e) => {
await fetch('/my/internal/bets', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(e.detail),
})
})
el.addEventListener('goma:bet-placed', (e) => console.log('receipt', e.detail))
</script><goma-game-details> Page-shaped widget rendering the full details for one sporting event: header (teams, score, clock), market-category tabs, and a scrollable list of collapsible market cards. Pair with <goma-events-horizontal> and card taps drive it via the existing goma:event-navigate event — no glue code.
pnpm add @gomagaming/game-details vue pinia vue-router vue-i18n @vueuse/core<div style="height: 100vh; display: grid; grid-template-rows: auto 1fr;">
<goma-events-horizontal id="evts"></goma-events-horizontal>
<goma-game-details id="gd"></goma-game-details>
</div>
<script type="module">
import '@gomagaming/events-horizontal'
import '@gomagaming/game-details'
const cfg = { apiBaseUrl: 'wss://sports-api.example.com', locale: 'en' }
document.getElementById('evts').config = { ...cfg, topic: 'live/1' }
document.getElementById('gd').config = cfg
// Tapping a card emits goma:event-navigate; game-details patches
// its eventId via the watcher — no glue code needed.
</script>Mount a sports picker and an events feed on the same page; the picker's goma:sport-select event automatically rewires the events feed's WAMP topic via followSportPicker. Either picker (horizontal or dialog) works — they share the same event contract.
<goma-sports-navigation-horizontal id="picker"></goma-sports-navigation-horizontal>
<goma-events-horizontal id="ev" followSportPicker></goma-events-horizontal>
<script type="module">
import '@gomagaming/sports-navigation-horizontal'
import '@gomagaming/events-horizontal'
// One config object drives both widgets — bundlers dedup the framework deps.
const config = {
socketUrl: 'wss://sportsapi.example.com/v2',
socketRealm: 'www.example.com',
apiBaseUrl: 'https://api.example.com',
bettingApiBaseUrl: 'https://sports-api.example.com',
ucsOperatorId: '4313',
locale: 'en',
}
const picker = document.getElementById('picker')
picker.config = config
picker.dataSource = 'subscribe'
picker.liveStatus = 'LIVE'
const events = document.getElementById('ev')
events.config = config
events.topic = 'custom-matches-aggregator/1/all/all/all/POPULAR/LIVE/10/5'
// `followSportPicker` listens for `goma:sport-select` from any picker on
// the page and rewrites the topic's sportId segment in place — no manual
// wiring needed.
</script>For responsive layouts that swap between the horizontal picker and the dialog at a breakpoint, see Sports navigation — deployment scenarios.
theme object; keys are auto-prefixed with --goma-. Full token reference in the Theming guide.ref callback), Vue (@goma:event syntax), and Angular all interop without wrappers. Per-framework snippets live on each widget's reference page.CustomEvents with composed: true. They cross the Shadow DOM boundary and bubble to the host, so window.addEventListener('goma:sport-select', …) works.