GiftingStack
Sign in Get started
Docs menu

Docs

Customize the SDK

Everything you can change about how the SDKs look and behave, with a short snippet for each of the five platforms: Web, React Native, Flutter, Android (Compose) and iOS (SwiftUI). Pick a platform in any tab row and the whole page follows.

Checked against the SDK sources (web, React Native, Flutter, Android, iOS) on 2026-10-20.

A "Looks like" line under each heading says what you will see, because this page has no screenshots. Anything that exists on one platform only is marked, and the table at the end sums it up.

On this page: Theme · Tray placement · Gift button in your message bar · Texts · Reduced motion · Low quality · Test mode · Get coins · Combo · Gift pictures · Effect tiers · Render slots · Battle bar · Wall, leaderboard, away · Inbox · More options · Custom elements · Platforms

Theme: one accent colour, light and dark

Every colour of the tray, the notice, the wall, the card and the leaderboard follows your brand colour, and the text on it stays readable.

Build a whole theme from one colour with themeFromAccent, or take a ready preset (dark or light). Text pairs are kept at 4.5:1 contrast. To change a single colour, override that token on top of a theme: the 17 tokens are background, surface, border, text, muted, accent, accentText, coin, coinText, selected, danger, noticeBackground, noticeText, comboText, noticeBigBackground, noticeBigText and glow. Pass the same theme to the tray and the effect layer (and to every other component).

Web

import { darkTheme, lightTheme, themeFromAccent } from "@giftingstack/sdk/web";

const theme = themeFromAccent("#ff4f9a", "light"); // or darkTheme, lightTheme
const effects = new GiftEffectLayer(screen, { theme });
const handle = createGiftTray({ container: screen, client, receiver, effects, theme });

handle.tray.update({ theme: { ...theme, coin: "#ffd27a" } }); // switch live, one token changed
effects.setOptions({ theme });

React Native

import { darkTheme, lightTheme, themeFromAccent } from "@giftingstack/sdk/react-native";

const theme = themeFromAccent("#ff4f9a", "light"); // or darkTheme, lightTheme
<GiftTray gifts={tray.gifts} balance={tray.balance} onSend={tray.send} theme={theme} />
<GiftEffectLayer ref={effects} theme={theme} />
// one token changed: theme={{ ...theme, coin: "#ffd27a" }}

Flutter

final theme = themeFromAccent('#ff4f9a', GiftThemeMode.light); // or darkTheme, lightTheme
GiftTray(gifts: tray.gifts, balance: tray.balance, onSend: tray.send, theme: theme)
GiftEffectLayer(controller: effects, theme: theme)
// one token changed:
final brand = theme.copyWith(coin: const Color(0xFFFFD27A));

Android (Compose)

val theme = GiftingTheme.fromAccent("#ff4f9a", dark = false) // or GiftingTheme.Dark, GiftingTheme.Light
GiftTray(gifts = state.gifts, balance = state.balance, onSend = { tray.send(it) }, theme = theme)
GiftEffectLayer(effects, theme = theme)
// one token changed:
val brand = theme.copy(coin = Color(0xFFFFD27A))

iOS (SwiftUI)

var theme = try GiftingTheme.fromAccent("#ff4f9a", mode: .light) // or GiftingTheme.dark, GiftingTheme.light
GiftTray(gifts: tray.gifts, balance: tray.balance, onSend: { tray.send($0) }, theme: theme)
GiftEffectLayer(controller: effects, theme: theme)
// one token changed:
theme.coin = Color(red: 1, green: 0.82, blue: 0.48)

On the web you can also set the CSS variables (--gs-accent, --gs-coin, --gs-surface and so on) from your own style sheet instead of passing a theme. A bad accent colour (not a hex value) throws.

Tray placement

By default the tray is a bottom sheet you can drag between three sizes; on a wide desktop page it can also be a popover under your gift button, or a panel inside your layout.

Every platform has the bottom sheet: peek (handle, balance and the Send bar), default (two rows of gifts) and expanded (up to 90 % of the screen). Choose where it opens, listen for changes and keep the effect notice above it with the inset callback. sheet set to false gives the old fixed tray in your layout.

Web

// Bottom sheet (the default below 600 px)
const handle = createGiftTray({
  container: screen, client, receiver, effects,
  initialSnap: "peek", // "peek" | "default" | "expanded"
  snaps: { expanded: 0.6 }, // heights as fractions of the container
  onInsetChange: (px) => effects.setOptions({ noticeBottom: px + 16 }),
  scrim: true, // a dim behind the sheet, hidden while it is at peek
  scrimTap: "dismiss", // a tap on the dim: "lower" (default) | "dismiss"
  onDismiss: () => {}, // the tap asked to close (the tray is already closed)
});
giftButton.onclick = () => handle.toggle(); // open if closed, close if open

// Desktop: a popover under your gift button, or inline in a column
createGiftTray({ container: playerColumn, anchor: giftButton, client, receiver, effects });
createGiftTray({ container: column, client, receiver, effects, placement: "inline" });
// placement: "auto" (default) | "sheet" | "popover" | "inline"

Popover and inline are web only. A popover needs an anchor and a container at least 600 px wide; Escape and a click outside close it.

React Native

<GiftTray
  tray={tray} // the holder from useGiftTray: its open state, toggle() and close() drive the sheet
  gifts={tray.gifts} balance={tray.balance} onSend={tray.send}
  initialSnap="peek" // "peek" | "default" | "expanded"
  snaps={{ expanded: 0.6 }}
  onSnapChange={(snap) => {}}
  onInsetChange={setInset} // pass inset + 16 as noticeBottom of the effect layer
  bottomInset={safeArea.bottom}
  scrim // a dim behind the sheet, hidden while it is at peek
  scrimTap="dismiss" // a tap on the dim: "lower" (default) | "dismiss"
  onDismiss={() => {}} // the tap asked to close (tray.close() already ran)
  // sheet={false}: the old fixed tray in the normal layout
/>
// Your Gift button: <Pressable onPress={tray.toggle} />
// tray = useGiftTray({ ..., open: false }) starts closed; also tray.open(), tray.close(), tray.isOpen

Flutter

GiftTray(
  tray: tray, // the GiftTrayNotifier: its open state, toggle() and close() drive the sheet
  gifts: tray.gifts, balance: tray.balance, onSend: tray.send,
  initialSnap: SheetSnap.peek, // peek | standard | expanded ("standard" is the "default" snap)
  snaps: const {SheetSnap.expanded: 0.6},
  onSnapChange: (snap) {},
  onInsetChange: (px) => setState(() => noticeBottom = px + 16),
  bottomInset: MediaQuery.paddingOf(context).bottom,
  scrim: true, // a dim behind the sheet, hidden while it is at peek
  scrimTap: ScrimTap.dismiss, // a tap on the dim: ScrimTap.lower (default) | dismiss
  onDismiss: () {}, // the tap asked to close (tray.close() already ran)
  // sheet: false, the old fixed tray in the normal layout
)
// Your Gift button: onPressed: tray.toggle
// GiftTrayNotifier(open: false) starts closed; also tray.open(), tray.close(), tray.isOpen

Android (Compose)

GiftTray(
    tray = tray, // the holder: gifts, balance, send and the open state (toggle(), close()) come from it
    initialSnap = SheetSnap.Peek, // Peek | Default | Expanded
    snaps = SheetSnapFractions(expanded = 0.6),
    onSnapChange = { snap -> },
    onInsetChange = { inset = it }, // GiftEffectLayer(noticeBottom = inset + 12.dp)
    bottomInset = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding(),
    scrim = true, // a dim behind the sheet, hidden while it is at peek
    scrimTap = ScrimTap.Dismiss, // a tap on the dim: ScrimTap.Lower (default) | Dismiss
    onDismiss = { }, // the tap asked to close (tray.close() already ran)
    // sheet = false: the old fixed tray in the normal layout
)
// Your Gift button: onClick = { tray.toggle() }
// the holder starts closed with open = false; also tray.open(), tray.close(), tray.isOpen

iOS (SwiftUI)

GiftTray(
    tray: tray, // the GiftTrayModel: gifts, balance, send and the open state (toggle(), close())
    gridMaxHeight: 230, // used with sheet: false, the old fixed tray
    sheet: true,
    initialSnap: .peek, // .peek | .default | .expanded
    snaps: nil, // SheetSnaps(...): heights as fractions
    onSnapChange: { snap in },
    onInsetChange: { noticeBottom = $0 + 16 },
    scrim: true, // a dim behind the sheet, hidden while it is at peek
    scrimTap: .dismiss, // a tap on the dim: .lower (default) | .dismiss
    onDismiss: { } // the tap asked to close (tray.close() already ran)
)
// Your Gift button: Button("Gift") { tray.toggle() }
// the holder starts closed with open: false; also tray.open(), tray.close(), tray.isOpen

Popover and inline are not on React Native, Flutter, Android or iOS: they use the bottom sheet only (see the table on SDKs).

Dim behind the sheet and your Gift button

scrim (off by default) draws a dim behind the sheet over the whole screen. It fades in over 200 ms (at once when the person turned animations off), goes away while the sheet is at peek so the page or the stream stays in sight, and takes the taps above the sheet, at peek too: a tap on the empty space beside a small tray closes it, and the next tap reaches your page. (Leave scrim off if the page must stay touchable beside a small tray.) scrimTap says what a tap on it does: "lower" (default) lowers a tall sheet to peek and closes a peeking one, "dismiss" closes the tray from every state; onDismiss tells you. Your Gift button is one line, tray.toggle(): it opens a closed tray and closes an open one (open(), close() and isOpen are there too, and open: false starts it closed). While it is closed the sheet and the dim are invisible and take no touches, and the inset callback says 0. The web popover keeps its own behaviour: Escape and a click outside close it.

Gift button in your message bar

A small gift button that sits in your own chat or comment bar, like an emoji button. Tap it: the keyboard goes down and the gift tray takes its place. Tap the text field: the tray goes away and the keyboard comes back.

GiftingStack has no chat of its own; it fits into yours. Put GiftButton inside or next to your text field and give the tray that field. The SDK then does the swap on every platform: the button closes the keyboard and opens the tray in the keyboard's place (on a phone about as tall as the keyboard was, docked at the bottom, with no dim, so the conversation stays in sight); a tap on the field closes the tray and the keyboard comes back; the button again does the same. With a field the tray starts closed. Everything else stays: collapseOnSelect, onSent, combos, and onInsetChange, which you use to keep your message bar on top of the tray as the keyboard would. On a wide web page the button opens the popover instead. The gift line in your chat is yours to draw: onSent gives you each of your own gifts, the room's events the others'.

// Web
const button = new GiftButton(messageBar, { theme }); // or <gift-button id="gift">
createGiftTray({ container: screen, client, receiver, effects,
  anchor: button.element, input: messageField, collapseOnSelect: true,
  onInsetChange: (px) => messageBar.style.setProperty("bottom", px + "px") });
// <gift-tray receiver="mia" anchor="#gift" input="#msg"></gift-tray>

// React Native
const field = useRef(null);
const tray = useGiftTray({ client, receiver, effects, input: field });
<TextInput ref={field} /> <GiftButton tray={tray} />
<GiftTray tray={tray} collapseOnSelect onInsetChange={setInset} />

// Flutter
final tray = GiftTrayNotifier(client: client, receiver: receiver, input: focusNode);
TextField(focusNode: focusNode), GiftButton(tray: tray), GiftTray(tray: tray)

// Android (Compose)
TextField(value, onValueChange, modifier = Modifier.giftTrayInput(holder))
GiftButton(tray = holder); GiftTray(tray = holder)

// iOS (SwiftUI)
@FocusState var typing: Bool
TextField("Message", text: $text).giftTrayInput(model, focused: $typing)
GiftButton(tray: model); GiftTray(tray: model)

GiftButton options: label (default "Send a gift", read by screen readers), filled (off: the glyph in your accent colour, like an emoji button; on: an accent circle), size (small 32, medium 36 or large 44: three fixed sizes, never a free number), icon (your own glyph instead of the gift, see below), disabled and theme, the same tokens as the tray. In a battle one button is enough: the tray's receivers option adds a "Send to" row with every side (how). While its tray is open it shows the selected colour and says "expanded". The tray learns the keyboard's height the first time the keyboard shows (300 until then, and never more than 60 % of the screen); pass your own snaps.default to choose the height yourself.

Your own icon

icon replaces the gift glyph; the circle, colours, states and tap target stay. The icon is centred in a fixed box (22 of 36 of the circle) and fitted into it: it is scaled down, never up, with its proportions kept, and anything outside the box is cut off, so your artwork can never change the size of the button. You get the colour the glyph would have (your accent colour, or the text-on-accent colour when filled), so a one-colour icon follows your theme.

// Web: SVG markup (currentColor follows the glyph colour) or an element (an img is not tinted)
new GiftButton(messageBar, { icon: '<svg viewBox="0 0 24 24"><path d="..." fill="currentColor"/></svg>', size: "large" });
// React Native: <GiftButton tray={tray} icon={(color) => <MyIcon color={color} />} />
// Flutter:      GiftButton(tray: tray, icon: (context, color) => Icon(Icons.star, color: color))
// Android:      GiftButton(tray = holder, icon = { color -> Icon(MyIcon, null, tint = color) })
// iOS:          GiftButton(tray: model, icon: { color in AnyView(Image("star").foregroundColor(color)) })

Texts and translations

Every word the SDK shows or speaks to a screen reader is yours: the tray title, the Send button, the card heading, the leaderboard tabs.

The tray takes its texts as plain options. The wall, the away card and the leaderboard take a labels object with the keys they use (for example awayTitle, dismiss, tryAgain, pinned, loading, and per-period tabs and empty texts). Unset ones stay English, and {name}-style places are filled in for you. The battle bar has its own labels (see below).

Web

createGiftTray({
  container: screen, client, receiver, effects,
  title: "Hediye gönder", coinName: "jeton", sendLabel: "Gönder",
  notEnoughLabel: "Yetersiz jeton", getCoinsLabel: "Jeton al", testModeLabel: "Test modu",
});
new GiftAwaySummary(screen, { client, effects, labels: { awayTitle: "Sen yokken", dismiss: "Kapat" } });

React Native

<GiftTray
  gifts={tray.gifts} balance={tray.balance} onSend={tray.send}
  title="Hediye gönder" coinName="jeton" sendLabel="Gönder"
  notEnoughLabel="Yetersiz jeton" getCoinsLabel="Jeton al" testModeLabel="Test modu"
/>
<GiftAwaySummary client={client} effects={effects} labels={{ awayTitle: "Sen yokken", dismiss: "Kapat" }} />

Flutter

GiftTray(
  gifts: tray.gifts, balance: tray.balance, onSend: tray.send,
  title: 'Hediye gönder', coinName: 'jeton', sendLabel: 'Gönder',
  notEnoughLabel: 'Yetersiz jeton', getCoinsLabel: 'Jeton al', testModeLabel: 'Test modu',
)
GiftAwaySummary(client: client, effects: effects,
    labels: const GiftLabels(awayTitle: 'Sen yokken', dismiss: 'Kapat'))

Android (Compose)

GiftTray(
    gifts = state.gifts, balance = state.balance, onSend = { tray.send(it) },
    title = "Hediye gönder", coinName = "jeton", sendLabel = "Gönder",
    notEnoughLabel = "Yetersiz jeton", getCoinsLabel = "Jeton al", testModeLabel = "Test modu",
)
GiftAwaySummary(client, effects = effects,
    labels = GiftLabels(awayTitle = "Sen yokken", dismiss = "Kapat"))

iOS (SwiftUI)

GiftTray(
    gifts: tray.gifts, balance: tray.balance, onSend: { tray.send($0) },
    title: "Hediye gönder", coinName: "jeton", sendLabel: "Gönder",
    notEnoughLabel: "Yetersiz jeton", getCoinsLabel: "Jeton al",
    testModeLabel: "Test modu"
)
var labels = GiftLabels()
labels.awayTitle = "Sen yokken"
labels.dismiss = "Kapat"
@StateObject var away = GiftAwayModel(client: client, labels: labels)

Web custom elements take the tray texts as attributes (tray-title, coin-name, send-label, not-enough-label, get-coins-label) and labels as a property.

Reduced motion

With reduced motion on, gifts do not fly: only the small notice ("ayla sent Rose") slides in, the cards appear without sliding, and on web and Flutter the sheet snaps without animating.

By default every platform follows the system setting (web: prefers-reduced-motion, live; Android: "Remove animations"; iOS: Reduce Motion; Flutter: MediaQuery.disableAnimations). Pass reducedMotion to force it on or off; every component takes the same option.

Web

new GiftEffectLayer(screen, { reducedMotion: true }); // true | false | leave out to follow the browser

React Native

<GiftEffectLayer ref={effects} reducedMotion />

Flutter

GiftEffectLayer(controller: effects, reducedMotion: true) // true | false | null (follows the system)

Android (Compose)

GiftEffectLayer(effects, reducedMotion = true) // true | false | null (follows the system)

iOS (SwiftUI)

GiftEffectLayer(controller: effects, reducedMotion: true) // true | false | nil (follows the system)

Low-quality mode for slow phones

Large gifts lose the coin rain and the light rays, and there are fewer sparks, so a slow phone keeps its frame rate; the gift picture and the notice are the same.

The app chooses: the SDK does not detect a slow device. Test on low-end phones and switch it on if needed.

Web

new GiftEffectLayer(screen, { quality: "low" });

React Native

<GiftEffectLayer ref={effects} quality="low" />

Flutter

GiftEffectLayer(controller: effects, quality: EffectQuality.low)

Android (Compose)

GiftEffectLayer(effects, quality = EffectQuality.Low)

iOS (SwiftUI)

GiftEffectLayer(controller: effects, quality: .low)

The away card replays gifts through your layer; give it the same quality so its replay is just as light.

Test mode badge

A small "Test mode" pill next to the tray title, shown while the client token was made with a test key, so testers know no real balance moves.

Pass the state holder to the tray (tray={tray}, tray: tray, tray = holder) and the badge shows by itself while GET /v1/client/me answers livemode: false. testMode is optional: unset follows the state, false forces the badge off, true forces it on. The badge text is testModeLabel. On the web, createGiftTray does this by itself.

Web

createGiftTray({ container: screen, client, receiver, effects, testModeLabel: "Test modu" });
// With the presentational class you pass it yourself:
tray.update({ testMode: true });

React Native

<GiftTray tray={tray} />

Flutter

GiftTray(tray: tray)

Android (Compose)

GiftTray(tray = holder)

iOS (SwiftUI)

GiftTray(tray: model)

The "Get coins" button

A "+ Coins" button sits by the balance, and when the balance is too low the Send button turns into "Get coins"; both call your function, which opens your own coin store.

GiftingStack sells no coins and takes no payment: your function opens your store, and your server credits the coins afterwards. Without onTopUp neither button is shown and a low balance shows "Not enough coins". The text is getCoinsLabel.

Web

createGiftTray({ container: screen, client, receiver, effects, onTopUp: openCoinStore });

React Native

<GiftTray gifts={tray.gifts} balance={tray.balance} onSend={tray.send} onTopUp={openCoinStore} />

Flutter

GiftTray(gifts: tray.gifts, balance: tray.balance, onSend: tray.send, onTopUp: openCoinStore)

Android (Compose)

GiftTray(gifts = state.gifts, balance = state.balance, onSend = { tray.send(it) }, onTopUp = { openCoinStore() })

iOS (SwiftUI)

GiftTray(gifts: tray.gifts, balance: tray.balance, onSend: { tray.send($0) }, onTopUp: { openCoinStore() })

On the web element, add the top-up attribute and listen for the gift-topup event.

Combo counter

Sending the same gift again and again turns the notice into a counter that pops: "ayla sent Rose" x2, x3, x4.

Notices stack like banners in a live stream: up to three lines at once, the newest at the bottom and older ones above it. Gifts from different people (or different gifts) each get their own line; a combo updates its own line in place. Every line leaves on its own after its time, and a fourth line pushes the oldest one out.

A combo is the same sender and the same gift again within a time window, 2,500 ms by default. Every sender and gift pair counts on its own, so several people can run a combo at the same time and each one keeps its own line. Change the window with comboWindowMs; it beats the number from remote recipes. On Android the window belongs to the layer state, on iOS to the effect controller.

Web

new GiftEffectLayer(screen, { comboWindowMs: 3000 });

React Native

<GiftEffectLayer ref={effects} comboWindowMs={3000} />

Flutter

GiftEffectLayer(controller: effects, comboWindowMs: 3000)

Android (Compose)

val effects = rememberGiftEffectLayerState(comboWindowMs = 3000)

iOS (SwiftUI)

@StateObject private var effects = GiftEffectController(comboWindowMs: 3000)

Your own gift pictures

Each gift shows its own picture in the tray, the lists and the notice, and a larger picture in the effect. Both come from the gift in your catalog, so one change in the dashboard updates every platform.

A gift has two picture fields: icon_url (the picture you see in the tray, lists and notices) and animation_url (the large picture shown with the gift effect). Both are optional. There are two ways to fill them:

  • Upload in the dashboard. Open Gifts, add or edit a gift and press Upload image next to the picture field. We store the image and fill the field with its address (on uploads.giftingstack.com). The Uploaded images section on the same page lists what you uploaded, how much of your allowance is used, and lets you copy an address or delete an image.
  • Use your own https URL. Paste the address of an image on your own server or CDN. The field takes any https address; nothing is uploaded to us.
Limit for uploads Value
File types PNG, JPEG and WebP
Size of one file 2 MB
Size of one image At most 2048 × 2048 pixels
Per project environment (test and live count separately) 200 images and 100 MB
Who can upload and delete The project owner. Team members see the list but cannot change it.

Deleting an uploaded image does not change your gifts: a gift that uses it shows nothing until you give it another picture, so check the gifts first. Uploaded images are public to anyone who has the address, like any picture your app shows. You are responsible for having the rights to what you upload (see the Terms of Service). Uploading is a dashboard feature; through the API you set icon_url and animation_url to a URL.

Effect tiers: which gifts get which effect

Cheap gifts float up with a small notice, mid-price gifts fly to the middle and burst into sparks, and the most expensive ones dim the screen with light rays, coin rain and a golden notice.

The effect depends on the gift's price in coins. By default under 100 is small, 100 to 9,999 is medium, 10,000 and up is large. Move the two limits with thresholds on the effect layer (and on the away card, so its replay matches). Large gifts play one at a time; small and medium ones overlap, with a cap.

Web

new GiftEffectLayer(screen, { thresholds: { mediumFrom: 50, largeFrom: 5000 } });

React Native

<GiftEffectLayer ref={effects} thresholds={{ mediumFrom: 50, largeFrom: 5000 }} />

Flutter

GiftEffectLayer(controller: effects, thresholds: const TierThresholds(mediumFrom: 50, largeFrom: 5000))

Android (Compose)

GiftEffectLayer(effects, thresholds = TierThresholds(mediumFrom = 50, largeFrom = 5000))

iOS (SwiftUI)

GiftEffectLayer(controller: effects, thresholds: TierThresholds(mediumFrom: 50, largeFrom: 5000))

In live rooms, minTier on the room ("medium" on web, an EffectTier value on the others) keeps small gifts off a busy screen; see Gift rooms.

Render slots: draw the inside yourself

The wall, the leaderboard and the away card keep their frame, loading, order, names, focus and accessibility; you draw what is inside each chip, each row or the card body.

Three hooks, the same names on all five platforms: renderChip (wall), renderRow (leaderboard) and renderSummary (away card). Return nothing (null, nil) and the default is drawn, so you can change some items only. Details and the full pattern are on Integration patterns.

Web

new GiftWall(slot, { client, user: "mateo", renderChip: ({ gift, count }) => `${gift.name} x${count}` });
new GiftLeaderboard(panel, { client, renderRow: ({ rank, name, scoreText }) => `#${rank} ${name} (${scoreText})` });
new GiftAwaySummary(screen, { client, effects, renderSummary: ({ headline }) => headline });

React Native

<GiftWall client={client} user="mateo" renderChip={({ gift, count }) => <Text>{gift.name} x{count}</Text>} />
<GiftLeaderboard client={client} renderRow={({ rank, name, scoreText }) => <Text>#{rank} {name} {scoreText}</Text>} />
<GiftAwaySummary client={client} effects={effects} renderSummary={({ headline, dismiss }) => <Pressable onPress={dismiss}><Text>{headline}</Text></Pressable>} />

Flutter

GiftWall(client: client, user: 'mateo', renderChip: (context, chip) => Text('${chip.gift.name} x${chip.countText}'))
GiftLeaderboard(client: client, renderRow: (context, row) => row.isMe ? null : MyRow(row.rank, row.name, row.scoreText))
GiftAwaySummary(client: client, renderSummary: (context, s) => MyCard(s.headline, onClose: s.dismiss))

Android (Compose)

GiftWall(client, user = "mateo", renderChip = { chip -> BasicText("${chip.gift.name} x${chip.countText}") })
GiftLeaderboard(client, renderRow = { row -> if (row.isMe) row.Default() else MyRow(row.rank, row.name, row.scoreText) })
GiftAwaySummary(client, renderSummary = { s -> MyCard(s.headline, onClose = s.dismiss) })

iOS (SwiftUI)

GiftWall(loader: walls, user: "mateo", renderChip: { chip in AnyView(Text("\(chip.gift.name) x\(chip.countText)")) })
GiftLeaderboard(model: board, renderRow: { row in row.isMe ? nil : AnyView(MyRow(row)) })
GiftAwaySummary(model: away, renderSummary: { s in AnyView(MyCard(s.headline, close: s.dismiss)) })

Web returns a DOM node or a string (a string is text, never HTML). A hook that throws falls back to the default on the web and reports to onError. With your own card body a tap does not dismiss it: call dismiss.

Battle bar: side colours and labels

A bar for a live battle round: the receivers with their pictures and scores, a countdown and a split bar whose boundary slides as gifts arrive. Two receivers are the classic versus; three to eight (a free-for-all where everybody races on their own) get one cell each in a row, the clock above and a bar of proportional coloured segments.

The bar draws what your server's round says and decides nothing itself; nothing is drawn while the room has no round with two sides. The side colours default to the theme's accent, its coin colour, then teal and pink, then the same four again: override them with sideColors, a list with one colour per side (a shorter list keeps the default for the sides it does not name; a pair works as before). Texts (vs, the final clock, the winner line, the draw line, the spoken summary) are labels. Name and picture of each receiver come from resolveUsers (web) or users (the others); without them the bar shows the ids. It follows the theme and reduced motion.

Web

import { GiftBattleBar, joinRoom } from "@giftingstack/sdk/web";

const room = joinRoom({ room: "live_42", getToken, client, effects });
new GiftBattleBar(stage, {
  room, // or call bar.setView(view) yourself
  resolveUsers: async (ids) => ({ ava: { name: "Ava", avatarUrl }, mateo: { name: "Mateo" } }),
  sideColors: ["#e0457b", "#3a8dff"],
  labels: { final: "Bitti", wins: "{name} kazandı" },
});

React Native

const room = useGiftRoom({ room: "live_42", getToken, client, effects });
const view = useBattleScore(room.controller);
<BattleBar
  view={view}
  users={{ ava: { name: "Ava" }, mateo: { name: "Mateo" } }}
  sideColors={["#e0457b", "#3a8dff"]}
  labels={{ final: "Bitti", wins: "{name} kazandı" }}
/>

Flutter

GiftBattleBar(
  room: room, // or view: myBattleView
  users: {'ava': BattleBarUser(name: 'Ava'), 'mateo': BattleBarUser(name: 'Mateo')},
  sideColors: const [Color(0xFFE0457B), Color(0xFF3A8DFF)],
  labels: const BattleBarLabels(finalText: 'Bitti', wins: '{name} kazandı'),
)

Android (Compose)

GiftBattleBar(
    room, // or GiftBattleBar(room.battleView(), ...)
    users = mapOf("ava" to BattleBarUser("Ava"), "mateo" to BattleBarUser("Mateo")),
    sideColors = Color(0xFFE0457B) to Color(0xFF3A8DFF),
    labels = BattleBarLabels(finalText = "Bitti", wins = "{name} kazandı"),
)

iOS (SwiftUI)

GiftBattleBar(
    room: room, // a GiftRoomModel
    users: ["ava": BattleBarUser(name: "Ava"), "mateo": BattleBarUser(name: "Mateo")],
    sideColors: ["#E0457B", "#3A8DFF"],
    labels: BattleBarLabels(finalText: "Bitti", wins: "{name} kazandı")
)

The web label for the final clock is final; on Flutter, Android and iOS it is finalText. The web element is <gift-battle-bar> with the attributes side-colors ("#e0457b,#3a8dff"), tick-ms and reduced-motion. Rounds are started and ended by your server; see Gift rooms.

In a battle the gift notice also says who got the gift ("sent Rose to Mia") and gets a 2 px border in that side's colour (the side colours above, for every side of the round; the text keeps its colour). Pass the same sideColors to the room as to the bar – joinRoom, useGiftRoom, GiftRoom, rememberGiftRoom, GiftRoomModel or <gift-room side-colors="#e0457b,#3a8dff"> – and give the room the same resolveName that names the senders, so the receiver has a name. Outside battles nothing changes. The user's own gift says who got it too when the tray knows the room: pass the room's receiverFor to the tray as receiverOf (web createGiftTray({ receiverOf: room.receiverFor }), React Native useGiftTray({ receiverOf: room.controller.receiverFor }), Flutter GiftTrayNotifier(receiverOf: room.receiverFor), Android rememberGiftTrayHolder(receiverOf = room::receiverFor), iOS GiftTrayModel(receiverOf: room.receiverFor)). If you play your own events, set receiver: { name, side, color } on the event.

Gift wall, leaderboard and "while you were away"

The wall is a row of small gift chips with counts under a post or on a profile, the card says "While you were away: 37 gifts from 12 people" and replays the biggest ones, and the leaderboard ranks senders or receivers with tabs and your own row pinned.

The options you will reach for first are below; every option is listed in the SDK's README for your platform. Wall: how many chips, in which order and how big. Leaderboard: which board, which period tabs, a coins or gifts switch, how many rows, and which user to highlight. Card: when it counts as seen, how many gifts it replays and for how long.

Web

new GiftWall(slot, { client, user: "mert", reference: "post_42", max: 4, order: "catalog", size: "sm" });
new GiftLeaderboard(panel, {
  client, board: "receivers", periods: ["week", "all"], metricToggle: true,
  limit: 20, highlightUser: "mert", coinName: "jeton",
});
new GiftAwaySummary(screen, {
  client, effects, ackOn: "finished", minGifts: 3, maxEffects: 3, budgetMs: 5000,
});

React Native

<GiftWall client={client} user="mert" reference="post_42" max={4} order="catalog" size="sm" />
<GiftLeaderboard
  client={client} board="receivers" periods={["week", "all"]} metricToggle
  limit={20} highlightUser="mert" coinName="jeton"
/>
<GiftAwaySummary client={client} effects={effects} ackOn="finished" minGifts={3} maxEffects={3} budgetMs={5000} />

Flutter

GiftWall(client: client, user: 'mert', reference: 'post_42', max: 4, order: WallOrder.catalog, size: GiftWallSize.sm)
GiftLeaderboard(
  client: client, board: LeaderboardBoard.receivers,
  periods: const [LeaderboardPeriod.week, LeaderboardPeriod.all],
  metricToggle: true, limit: 20, highlightUser: 'mert', coinName: 'jeton',
)
GiftAwaySummary(client: client, effects: effects, ackOn: AwayAckOn.finished, maxEffects: 3, budgetMs: 5000)

Android (Compose)

GiftWall(client, user = "mert", reference = "post_42", max = 4, order = WallOrder.Catalog, size = GiftWallSize.Sm)
GiftLeaderboard(
    client, board = LeaderboardBoard.Receivers,
    periods = listOf(LeaderboardPeriod.Week, LeaderboardPeriod.All),
    metricToggle = true, limit = 20, highlightUser = "mert", coinName = "jeton",
)
GiftAwaySummary(client, effects = effects, ackOn = AwayAckOn.Finished, maxEffects = 3, budgetMs = 5000)

iOS (SwiftUI)

GiftWall(loader: walls, user: "mert", reference: "post_42", max: 4, order: .catalog, size: .sm)

@StateObject var board = GiftLeaderboardModel(client: client, board: .receivers, periods: [.week, .all], limit: 20)
GiftLeaderboard(model: board, metricToggle: true, highlightUser: "mert", coinName: "jeton")

@StateObject var away = GiftAwayModel(client: client, ackOn: .finished, maxEffects: 3, budgetMs: 5000)
GiftAwaySummary(model: away)

minGifts (no card for fewer gifts, but they are still acknowledged) exists on web and React Native only. The three components share one request when several are on screen, cache answers (wall 60 s, leaderboard tabs 30 s) and never show an error the person cannot act on.

Inbox

A list of who sent the signed-in person which gift over the last 24 hours: one row per gift, newest first, with the sender's initial, "Ayla sent you Rose x3", the gift picture and how long ago on the right.

It loads its own first page, shows "Show more" while there are older gifts and says "No gifts in the last 24 hours" when there are none. Pass resolveUsers to show display names and avatars instead of ids, and pageSize (1 to 100, default 20) for the rows per page. Gifts that were reversed are not listed. There is no "read" state on the server: keep the time of the newest row you showed and call inboxUnread(items, lastSeenMs) from onLoaded for a badge on your Inbox tab. refresh() loads the list again, for example after an incoming gift. Without a component, client.getInbox({ cursor, limit }) returns the same page.

Web

const inbox = new GiftInbox(panel, {
  client, pageSize: 20, resolveUsers,
  onLoaded: (items) => setBadge(inboxUnread(items, lastSeenMs)),
});
inbox.refresh();

React Native

<GiftInbox
  ref={inboxRef} client={client} pageSize={20} resolveUsers={resolveUsers}
  onLoaded={(items) => setBadge(inboxUnread(items, lastSeenMs))}
/>

Flutter

GiftInbox(client: client, theme: theme)

Android (Compose)

GiftInbox(client = client, theme = theme)

iOS (SwiftUI)

GiftInbox(model: GiftInboxModel(client: client))

More options

Small things you can set on the tray or the effect layer without a section of their own.

Option What it does Where
noticeBottom How high the "ayla sent Rose" notice sits above the bottom edge (default 150). Set it to the sheet's inset plus 16, from onInsetChange, so it stays above the tray. All five
columns, gridMaxHeight, defaultSelectedId, disabled Grid columns (4 by default), the height of the fixed tray (sheet set to false), the gift selected first, and turning sending off while your own request runs. All five (web: columns and the others on the tray options; attributes on <gift-tray>)
origin, pictureWaitMs Where large gifts start flying from, and how long an effect waits for its picture (300 ms). All five (on the effect layer)
pictureScale Draws the gift picture bigger in a wide layer such as a tablet. Automatic by default (1 on a phone, up to 2). Web (and picture-scale on the element), Flutter, Android and iOS; not React Native
remoteRecipes, recipesVersion, recipesUrl The effect numbers (durations, sparks, scale) are fetched from our recipes file so we can tune them without an app update. remoteRecipes set to false makes no request at all; a version pins a file; a URL self-hosts it. Your own thresholds and comboWindowMs always win. All five
autoCheck, focusOnShow Whether the away card checks by itself at start and on return to the app, and whether it moves keyboard or screen reader focus to the card when it appears. All five
refreshIntervalMs Polling interval of the leaderboard (0, no polling, by default). All five

Custom elements Web only

The same components as plain HTML tags, so a page without a framework can use them by writing attributes.

import "@giftingstack/sdk/elements" registers the tags (or call defineGiftElements()). The client is a property, never an attribute; so are theme, labels, resolveUsers and options (what attributes cannot say, such as snaps or a render function). Events bubble.

<gift-effect-layer id="fx" quality="low" combo-window-ms="3000"></gift-effect-layer>
<gift-tray receiver="mert" reference="post-42" effects="#fx" top-up
           tray-title="Hediye gönder" send-label="Gönder" placement="auto"></gift-tray>
<gift-battle-bar side-colors="#e0457b,#3a8dff"></gift-battle-bar>

<script type="module">
  import "@giftingstack/sdk/elements";
  const tray = document.querySelector("gift-tray");
  tray.client = client;
  tray.theme = themeFromAccent("#ff4f9a", "dark");
  tray.addEventListener("gift-topup", openCoinStore);
</script>
Tag Attributes Events
<gift-effect-layer> quality, reduced-motion, picture-scale, notice-bottom, combo-window-ms, remote-recipes, recipes-version, recipes-url gift-effect-end
<gift-tray> receiver, reference, effects, play-on, tray-title, coin-name, send-label, not-enough-label, get-coins-label, columns, grid-max-height, default-selected-id, disabled, sheet="false", placement, anchor, initial-snap, top-up gift-sent, gift-error, gift-state, gift-topup, gift-open-change, gift-snap-change, gift-placement-change, gift-inset-change
<gift-wall> user, reference, max, order, size, refresh-key, pressable gift-chip-press, gift-error
<gift-away-summary> effects, auto-check, ack-on, max-effects, budget-ms, quality, reduced-motion, focus-on-show, min-gifts gift-away-show, gift-away-done, gift-error
<gift-leaderboard> board, periods ("day,week,all"), initial-period, metric, metric-toggle, limit, highlight-user, refresh-interval-ms, coin-name, pressable gift-user-press, gift-error
<gift-battle-bar> side-colors, tick-ms, reduced-motion
<gift-room> room, effects, min-tier, skip-own, max-replay-age-ms gift-room-event, gift-room-viewers, gift-room-gap, gift-room-state

React Native, Flutter, Android and iOS have no custom elements: they use the components and widgets shown above.

What is on which platform

Consistent with the table on SDKs. "yes" means the same name or a close one in every snippet above.

                                   Web   RN    Flutter Android iOS
Theme (accent, presets, tokens)    yes   yes   yes     yes     yes
Tray as bottom sheet               yes   yes   yes     yes     yes
Tray as popover / inline           yes   -     -       -       -
Texts and translations             yes   yes   yes     yes     yes
Reduced motion                     yes   yes   yes     yes     yes
Low-quality mode                   yes   yes   yes     yes     yes
Test mode badge                    yes   yes   yes     yes     yes
"Get coins" button                 yes   yes   yes     yes     yes
Combo window                       yes   yes   yes     yes     yes
Effect tiers (thresholds)          yes   yes   yes     yes     yes
Render slots                       yes   yes   yes     yes     yes
Battle bar sides and labels        yes   yes   yes     yes     yes
Away card: minGifts                yes   yes   -       -       -
Gift inbox                         yes   yes   yes     yes     yes
Custom elements (tags)             yes   -     -       -       -

Setup, tokens and the first snippet per platform are there too; the three ways to put the components in your app are on Integration patterns.

Next: Integration patterns, SDKs and Gift rooms.