Skip to content

gomawtFramework-agnostic sportsbook widgets

A sports feed UI distributed as Web Components — embed with one import and a config object.

Quick examples

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.

WidgetPackageReference
<goma-events-horizontal>@gomagaming/events-horizontalAPI docs
<goma-sports-navigation-horizontal>@gomagaming/sports-navigation-horizontalAPI docs
<goma-sports-navigation-dialog>@gomagaming/sports-navigation-dialogAPI docs
<goma-betslip-floating>@gomagaming/betslip-floatingAPI docs
<goma-betslip-sidebar>@gomagaming/betslip-sidebarAPI docs
<goma-game-details>@gomagaming/game-detailsAPI 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.

bash
pnpm add @gomagaming/events-horizontal vue pinia vue-router vue-i18n @vueuse/core
html
<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).

bash
pnpm add @gomagaming/sports-navigation-horizontal vue pinia vue-router vue-i18n @vueuse/core
html
<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.

bash
pnpm add @gomagaming/sports-navigation-dialog vue pinia vue-router vue-i18n @vueuse/core
html
<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.

bash
pnpm add @gomagaming/betslip-floating vue pinia vue-router vue-i18n @vueuse/core
html
<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.

bash
pnpm add @gomagaming/betslip-sidebar vue pinia vue-router vue-i18n @vueuse/core
html
<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.

bash
pnpm add @gomagaming/game-details vue pinia vue-router vue-i18n @vueuse/core
html
<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>

End-to-end: events feed driven by a sports picker

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.

html
<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.

Cross-cutting concerns

  • Theming — assign a theme object; keys are auto-prefixed with --goma-. Full token reference in the Theming guide.
  • Framework integration — every widget is a real custom element, so React (ref callback), Vue (@goma:event syntax), and Angular all interop without wrappers. Per-framework snippets live on each widget's reference page.
  • Events — every widget emits CustomEvents with composed: true. They cross the Shadow DOM boundary and bubble to the host, so window.addEventListener('goma:sport-select', …) works.