The App Bridge UI Kit styles content inside your embedded app with tb- classes and light and dark themes. The stylesheet works with plain HTML and any framework. Optional form helpers, such as enhanced selects, file inputs, and date pickers, use JavaScript from the App Bridge SDK. Host-rendered controls are covered separately in UI primitives.
Install the stylesheet#
Link the stylesheet below and use tb- classes in your HTML. Load https://bridge.tringify.com/app-bridge@1.js before calling any TringifyBridge.ui helper. Styling your app does not grant access to host operations; use bridge.supports() after startup to check those capabilities.
<link rel="stylesheet" href="https://bridge.tringify.com/app-bridge-ui@1.css">
Follow the merchant's theme#
The kit renders light by default and follows data-theme on the root element. Set it from the App Bridge context when your app boots, and keep it updated on theme:change.
const bridge = TringifyBridge.init();
const context = await bridge.ready();
document.documentElement.dataset.theme = context.theme;
bridge.on("theme:change", (theme) => {
document.documentElement.dataset.theme = theme;
});
Design tokens#
Components use the CSS variables below for colors, borders, spacing, and corners. Setting data-theme="dark" on the root element switches their values automatically. Keep the defaults for a consistent Tringify appearance in both themes.
Variable |
Role |
|---|---|
|
Page background and foreground text |
|
Card surfaces |
|
Muted surfaces and secondary text |
|
Borders and dividers |
|
Primary buttons and solid badges |
|
Hover and selected surfaces |
|
Focus rings |
|
Corner radius base (0.5rem) |
Type scale#
The kit supplies the type sizes below. Use heading classes for headings, labels for form controls, and description classes for supporting text. Body text inherits a 16px size.
Class |
Size / weight |
Use |
|---|---|---|
|
24px / 600 |
Card or section title |
|
16px / 600 |
Compact section heading |
|
14px / 500 |
Form label |
|
14px / 400 |
Supporting description |
|
14px |
Form control or action |
|
12px / 400 |
Form help text |
|
14px / 550 |
Inline link |
|
13px / 400 |
Monospace identifiers |
unclassed text |
16px / 400 |
Body text |
Page scaffold#
Use one tb-page root; it fills the admin frame. Inside it, tb-page-header holds the title row, and tb-section blocks separate areas with a divider. Headings come in three sizes: tb-h1, tb-h2, and tb-h3.
<main class="tb-page">
<header class="tb-page-header">
<div>
<h1 class="tb-h1">Campaigns</h1>
<p class="tb-section-description">Schedule and review outgoing campaigns.</p>
</div>
<div class="tb-page-actions">
<button class="tb-btn">New campaign</button>
</div>
</header>
<section class="tb-section">…</section>
</main>
Header#
tb-header is the view header idiom: an optional back control on the left, a title with an optional subtitle, and actions on the right. The back chevron is drawn by the stylesheet — the button element stays empty apart from its accessible label.
<header class="tb-header"><button class="tb-header-back" type="button" aria-label="Back to campaigns"></button><div class="tb-header-titles"><h1 class="tb-header-title">Spring launch</h1><p class="tb-header-subtitle">Draft · last edited 2 hours ago</p></div><div class="tb-header-actions"><button class="tb-btn tb-btn-outline">Preview</button><button class="tb-btn">Publish</button></div></header>
Settings groups and rows#
Configuration screens compose from tb-settings-stack, tb-settings-group, and tb-settings-row. A row's summary line pairs a title and current value with an action on the right; an optional tb-settings-row-editor expands below it for inline editing. Toggle the editor with the hidden attribute.
<div class="tb-settings-stack">
<div class="tb-settings-group">
<div class="tb-settings-group-header">
<h2 class="tb-h2">Delivery</h2>
<p class="tb-section-description">How campaign email is sent.</p>
</div>
<div class="tb-settings-row">
<div class="tb-settings-row-summary">
<div>
<div class="tb-settings-row-title"><span>Sender name</span><span class="tb-badge tb-badge-muted">Default</span></div>
<div class="tb-settings-row-value">Acme Prints</div>
</div>
<div class="tb-settings-row-action">
<button class="tb-btn tb-btn-sm tb-btn-outline">Edit</button>
</div>
</div>
<div class="tb-settings-row-editor" hidden>
<div class="tb-field"><label class="tb-label">Sender name</label><input class="tb-input" value="Acme Prints"></div>
<div class="tb-settings-row-editor-actions">
<button class="tb-btn tb-btn-sm tb-btn-ghost">Cancel</button>
<button class="tb-btn tb-btn-sm">Save</button>
</div>
</div>
</div>
</div>
</div>
Forms#
tb-field wraps a tb-label with its control. tb-field-grid lays fields out two per row; add tb-field-span to a field to span the full width. tb-field-help carries helper text and tb-charcount a character counter — add tb-charcount-over past the limit. For multi-line template editing, pair tb-textarea with clickable tb-varchip variable chips.
<div class="tb-field-grid">
<div class="tb-field">
<label class="tb-label">Subject</label>
<input class="tb-input" maxlength="120" placeholder="Order update">
<p class="tb-field-help">Shown in the recipient's inbox.</p>
</div>
<div class="tb-field">
<label class="tb-label">Send window</label>
<input class="tb-input" value="09:00–17:00">
</div>
<div class="tb-field tb-field-span">
<label class="tb-label">Body</label>
<textarea class="tb-textarea">Hi {{first_name}}, your order shipped.</textarea>
<div class="tb-varchips">
<button class="tb-varchip" type="button">{{first_name}}</button>
<button class="tb-varchip" type="button">{{order_number}}</button>
</div>
<div class="tb-charcount">64 / 500</div>
</div>
</div>
Forms compose from cards, the same shape Storeadmin's own product and vendor forms use: a tb-card per section with a tb-card-header (tb-card-title and optional tb-card-description) and the fields in tb-card-body. Field grids come in two (tb-field-grid), three (tb-field-grid-3), and four (tb-field-grid-4) columns, collapsing on small screens. Close the form with tb-form-actions. For a flat form without cards, tb-fieldset groups divide sections with rules instead.
<div class="tb-card"><div class="tb-card-header"><h3 class="tb-card-title">Print output</h3><p class="tb-card-description">Final print dimensions in pixels, and the DPI it prints at.</p></div><div class="tb-card-body"><div class="tb-field-grid-3"><div class="tb-field"><label class="tb-label">Width (px)</label><input class="tb-input" type="number"></div><div class="tb-field"><label class="tb-label">Height (px)</label><input class="tb-input" type="number"></div><div class="tb-field"><label class="tb-label">Print DPI</label><input class="tb-input" type="number" value="300"></div></div></div></div><div class="tb-card"><div class="tb-card-header"><h3 class="tb-card-title">Artwork</h3></div><div class="tb-card-body"><div class="tb-field"><label class="tb-label">File</label><input class="tb-input" type="file"></div></div></div><div class="tb-form-actions"><button class="tb-btn tb-btn-sm tb-btn-outline">Cancel</button><button class="tb-btn tb-btn-sm">Publish</button></div>
For a select, keep a real <select class="tb-input"> inside its tb-field, then pass it to TringifyBridge.ui.enhanceSelect(). Give the native field a label and a name for form submission. A disabled, selected empty option acts as a placeholder and is omitted from the list. An enabled empty option remains selectable, for example “None”.
<div class="tb-field">
<label class="tb-label" for="pickup">Pickup location</label>
<select class="tb-input" id="pickup" name="pickup" required>
<option value="" disabled selected>Choose a pickup location</option>
<optgroup label="Available locations">
<option value="warehouse">Main warehouse</option>
<option value="studio">Print studio</option>
</optgroup>
</select>
</div>
<script>
const pickup = TringifyBridge.ui.enhanceSelect(document.getElementById("pickup"));
</script>
Selects. The enhanced list supports option groups, disabled options and groups, arrow keys, Home/End, and typing the start of a choice to focus it. Enter or Space selects; Escape closes without changing the value. It supports one choice only: multiple and size > 1 are rejected with INVALID_ARGUMENT. Options come from your own HTML; the helper does not fetch store records or provide remote search.
TringifyBridge.ui.enhanceSelect(document.getElementById("region"));
File pickers. enhanceFile(input, options) adds a browse-and-drop control, a list of every chosen filename and size, and Clear files. Use the native multiple and accept attributes. Drops with too many files or a type outside accept preserve the previous selection and show an error. The native input remains the source of files, input, and change events. Browser selection uses the platform file chooser; validate file content, type and size in your app and upload service. Choosing files does not upload them or add them to a store library. To choose existing Media or Site Assets, use the host media picker.
const artwork = document.getElementById("artwork"); // <input type="file" multiple accept="image/png,image/jpeg">
const files = TringifyBridge.ui.enhanceFile(artwork, {
cta: "Choose artwork",
hint: "PNG or JPEG files",
onReject(error) {
// Drop errors: TOO_MANY_FILES or FILE_TYPE_NOT_ALLOWED.
// The picker already displays an error; add app-specific handling here.
},
});
artwork.addEventListener("change", () => {
const chosenFiles = Array.from(artwork.files);
// Validate and upload these files through your app's upload flow.
});
// After assigning artwork.value or artwork.files in your own code:
files.sync();
// Before unmounting the field:
// files.destroy();
Character counters. enhanceCharCount reads the element's maxlength (or data-maxlength) and renders a live tb-charcount counter in Storeadmin's 12/80 format, flagging overflow with tb-charcount-over. Call the returned sync() after setting the value programmatically. To wire every limited field at once, call enhanceCharCounts(root?) — it sweeps the document (or a given element) for fields declaring maxlength or data-maxlength, skipping search inputs and fields marked data-charcount="off". Declare the limit on the field; the sweep does the rest.
TringifyBridge.ui.enhanceCharCounts(document);
Dates. Enhance a native <input type="date"> to choose one calendar day. The helper respects min, max, step, required, readonly, and disabled fields. Values remain YYYY-MM-DD calendar strings without a timezone conversion. Month and weekday names follow locale; the week starts according to that locale when the browser supports it, or Sunday otherwise. Set weekStartsOn explicitly for a fixed layout. Arrow keys move days, Home/End move within the week, Page Up/Down move months, and Shift + Page Up/Down move years. Enter or Space chooses a valid day. Today follows the field bounds and step; Clear is available when the field is optional. Escape and outside clicks dismiss without selecting. Date ranges, times, and datetimes are not provided.
const expiry = document.getElementById("expires");
const tomorrow = new Date();
tomorrow.setDate(tomorrow.getDate() + 1);
const dateValue = (date) => [
date.getFullYear(),
String(date.getMonth() + 1).padStart(2, "0"),
String(date.getDate()).padStart(2, "0"),
].join("-");
expiry.min = dateValue(tomorrow);
const datePicker = TringifyBridge.ui.enhanceDate(expiry, {
label: "Choose expiry date",
locale: "en-GB",
weekStartsOn: 1, // 0 = Sunday, 1 = Monday, …, 6 = Saturday
placeholder: "No expiry",
});
// after clearing the form programmatically:
expiry.value = "";
datePicker.sync();
Buttons#
tb-btn is the primary button. Variants compose: tb-btn-outline, tb-btn-ghost, tb-btn-icon, tb-btn-sm, and tb-btn-block. Disabled buttons need only the disabled attribute.
<div class="tb-row">
<button class="tb-btn">Save</button>
<button class="tb-btn tb-btn-outline">Preview</button>
<button class="tb-btn tb-btn-ghost">Cancel</button>
<button class="tb-btn tb-btn-sm" disabled>Publishing…</button>
</div>
Data display#
Resource tables#
Use the tb-resource-list composition for searchable, paginated record lists. It matches Store Admin resource pages as one complete layout: tb-resource-list-toolbar contains the search and filters, tb-resource-list-content contains either a semantic tb-resource-table or a tb-resource-list-empty state, and tb-resource-list-footer contains the result range, page-size control, and first, previous, page, next, and last controls. Build an empty state with tb-resource-list-empty-title, tb-resource-list-empty-text, and, when an action is useful, tb-resource-list-empty-actions. Keep server-side search and pagination authoritative. Enhanced select menus measure the embedded viewport when opened and automatically appear above the trigger when there is not enough room below; apps do not need placement logic. Use tb-table for tables that do not need this list workflow; it shares the same Store Admin table styling. Any list with more than one page should use tb-resource-list-footer, not previous and next alone, so merchants can jump to a page or change the page size.
<section class="tb-resource-list">
<div class="tb-resource-list-toolbar">
<div class="tb-resource-list-search">
<input class="tb-input" type="search" aria-label="Search products" placeholder="Search products">
</div>
</div>
<div class="tb-resource-list-content">
<div class="tb-resource-table-frame">
<table class="tb-resource-table">
<caption>Published products</caption>
<thead><tr><th scope="col">Product</th><th scope="col">Status</th><th scope="col" class="tb-resource-actions">Actions</th></tr></thead>
<tbody><tr>
<td><span class="tb-resource-primary">TrailForge Bottle</span><span class="tb-resource-secondary">SKU TF-100</span></td>
<td><span class="tb-resource-status">Active</span></td>
<td class="tb-resource-actions"><span class="tb-resource-actions-inner"><button class="tb-btn tb-btn-sm tb-btn-ghost">View</button></span></td>
</tr></tbody>
</table>
</div>
</div>
<footer class="tb-resource-list-footer">
<span class="tb-resource-list-range">1–25 of 42</span>
<div class="tb-resource-list-pager">
<label class="tb-resource-list-page-size">Per page<select class="tb-input"><option>25</option><option>50</option></select></label>
<div class="tb-resource-list-page-nav">
<button class="tb-resource-page-first" type="button" aria-label="First page" disabled></button>
<button class="tb-resource-page-prev" type="button" aria-label="Previous page" disabled></button>
<input class="tb-input tb-resource-list-page-input" inputmode="numeric" aria-label="Page" value="1">
<span class="tb-resource-list-page-count">/ 2</span>
<button class="tb-resource-page-next" type="button" aria-label="Next page"></button>
<button class="tb-resource-page-last" type="button" aria-label="Last page"></button>
</div>
</div>
</footer>
</section>
Use tb-tabs-scroll, tb-tabs, tb-tab, and tb-tabpanel for distinct views within one app page. The scroll wrapper keeps every tab reachable in a narrow embedded viewport. Give the tab list, tabs, and panels their corresponding ARIA roles and connect each tab to its panel with aria-controls and aria-labelledby. Set aria-selected on the active tab and hidden on inactive panels. For settings forms, place the editable controls in one tb-card with tb-card-header and tb-card-body, matching Store Admin Settings. For read-only record sets such as provider mappings, use tb-resource-list and tb-resource-table; add tb-resource-table-center when the first identity column should remain left-aligned while the remaining data columns are centered.
Tables go inside tb-table-wrap for the border and horizontal overflow. tb-table-cell-wrap lets a column wrap; tb-table-cell-actions right-aligns a compact actions column. For record lists that are not tabular, use tb-data-list rows; for headline numbers use tb-stat-grid — each tb-stat renders as its own card. tb-badge (with tb-badge-muted and tb-badge-solid) labels categories. For state, use tb-status-text, Store Admin's status indicator: a 7px dot beside foreground text. Add tb-status-success, tb-status-warning, tb-status-destructive or tb-status-info to color the dot; with no tone it stays neutral. tb-table uses Store Admin's list-table styling: a plain header row in medium-weight text, 14px rows and row dividers. When a row opens a record, add tb-table-row-link to the tr, give it tabindex="0", and open it on click and Enter instead of adding a View button. tb-table aligns columns by position: the first column reads left, the last aligns right, and every column between is centered under its heading. tb-table-center is no longer needed.
<div class="tb-stat-grid">
<div class="tb-stat"><span class="tb-stat-value">1,204</span><span class="tb-stat-label">Sent</span></div>
<div class="tb-stat"><span class="tb-stat-value">98.2%</span><span class="tb-stat-label">Delivered</span></div>
</div>
<div class="tb-table-wrap">
<table class="tb-table">
<thead><tr><th>Campaign</th><th>Status</th><th class="tb-table-cell-actions"></th></tr></thead>
<tbody>
<tr>
<td class="tb-table-cell-wrap">Spring launch</td>
<td><span class="tb-badge tb-badge-solid">Live</span></td>
<td class="tb-table-cell-actions"><button class="tb-btn tb-btn-sm tb-btn-outline">Open</button></td>
</tr>
</tbody>
</table>
</div>
Row lists. tb-rowlist frames a divided stack of tb-rowlist-item records — the standard shape for per-record status screens. Pair each row's title with a tb-badge tb-badge-muted qualifier and a tb-status-text state; add a tb-status-* tone when the state needs color.
<div class="tb-rowlist">
<article class="tb-rowlist-item">
<div class="tb-settings-row-summary">
<div>
<div class="tb-settings-row-title"><h3 class="tb-h3">Product feed</h3><span class="tb-badge tb-badge-muted">Required</span><span class="tb-status-text">Needs attention</span></div>
<div class="tb-settings-row-value">1,169 of 1,204 resolved · 33 missing · 2 conflicts</div>
</div>
<div class="tb-settings-row-action"><button class="tb-btn tb-btn-sm tb-btn-ghost">Review</button></div>
</div>
</article>
<article class="tb-rowlist-item">
<div class="tb-settings-row-summary">
<div>
<div class="tb-settings-row-title"><h3 class="tb-h3">Conversion events</h3><span class="tb-status-text">Ready</span></div>
<div class="tb-settings-row-value">All events delivered</div>
</div>
</div>
</article>
</div>
Group multiple row lists with standard tb-settings-group headers — one group per section. For dense read-only reference data (term → value), use tb-deflist, a two-column definition list inside a card; reach for a row list only when rows carry actions.
<div class="tb-card"><div class="tb-card-body-compact"><dl class="tb-deflist">
<dt>Availability</dt><dd>Inventory status</dd>
<dt>Price</dt><dd>Regular price</dd>
<dt>Link</dt><dd>Product URL</dd>
</dl></div></div>
Tabs and toolbars#
Drawer. Row-scoped editing and detail views slide in from the right with tb-drawer — the list underneath never reflows. Structure: a tb-drawer-root overlay holding a tb-drawer-backdrop and the tb-drawer sheet (tb-drawer-header with title + tb-drawer-close, a scrolling tb-drawer-body, and a pinned tb-drawer-footer for actions). To open: unhide the root, then add is-open on the next animation frame. To close: remove is-open, hide the root after the transition. Wire Escape and backdrop clicks to close. Rule of thumb: edit a row in a drawer; edit a whole resource on its own view. The stylesheet provides appearance and transitions; your app provides modal behavior. Give the drawer an accessible name, move focus inside on open, keep keyboard focus inside while it is modal, prevent interaction with the background, and restore focus to the trigger on close. Support Escape and reduced-motion preferences.
<div style="position:relative;height:340px;overflow:hidden;border:1px solid #e5e5e5;border-radius:8px">
<div class="tb-drawer-root is-open" style="position:absolute">
<div class="tb-drawer-backdrop"></div>
<aside class="tb-drawer" role="dialog" style="position:absolute;width:min(360px,80%)">
<header class="tb-drawer-header"><h2 class="tb-drawer-title">Description</h2><button class="tb-drawer-close" aria-label="Close"></button></header>
<div class="tb-drawer-body">
<div class="tb-field"><label class="tb-label">Source</label><select class="tb-input"><option>Catalog field</option></select></div>
<div class="tb-field" style="margin-top:16px"><label class="tb-label">Catalog field</label><select class="tb-input"><option>SEO Description</option></select></div>
</div>
<footer class="tb-drawer-footer"><button class="tb-btn tb-btn-sm tb-btn-outline">Cancel</button><button class="tb-btn tb-btn-sm">Save</button></footer>
</aside>
</div>
</div>
tb-tabs is a segmented tab list; mark the active tab with aria-selected true and pair each tab with a tb-tabpanel, hiding inactive panels with the hidden attribute. tb-toolbar holds search, filters and actions above a table, with tb-toolbar-group clustering related controls. A tb-search inside it fills the row, like Store Admin's list search; filters and actions wrap beside it. Page headers (tb-header) are separated from content by spacing, not a rule.
<div class="tb-tabs" role="tablist">
<button class="tb-tab" role="tab" aria-selected="true">Active</button>
<button class="tb-tab" role="tab" aria-selected="false">Archived</button>
</div>
<div class="tb-tabpanel">…</div>
<div class="tb-tabpanel" hidden>…</div>
States#
Prefer a skeleton over the spinner when the eventual layout is known: compose tb-skeleton blocks into that layout with the size utilities (tb-skeleton-title, tb-skeleton-text, tb-skeleton-btn, tb-skeleton-input, tb-skeleton-row, tb-skeleton-square, tb-skeleton-circle), or use tb-skeleton-list for a list of loading rows. The pulse matches Storeadmin's own skeletons and pauses under reduced-motion preferences.
<header class="tb-header"><div class="tb-header-titles"><div class="tb-skeleton tb-skeleton-title"></div><div class="tb-skeleton tb-skeleton-text" style="margin-top:8px"></div></div><div class="tb-header-actions"><div class="tb-skeleton tb-skeleton-btn"></div></div></header><div class="tb-skeleton-list"><div class="tb-skeleton"></div><div class="tb-skeleton"></div><div class="tb-skeleton"></div></div>
tb-loading renders a centered spinner with a label while a view waits for data. tb-empty is the dashed empty state. tb-note carries an inline notice inside your page — for status that should sit above the app in Storeadmin's chrome, use bridge.banner.show() from UI primitives instead. Components carry no outer margins — wrap sibling blocks in tb-stack for vertical rhythm.
<div class="tb-stack"><div class="tb-loading">Loading campaigns</div><div class="tb-empty">No campaigns yet. Create your first campaign to get started.</div><div class="tb-note">Sending is paused while the domain is verifying.</div></div>
Landing pages#
An app's home screen introduces the app, shows connection state, and walks the merchant through setup. These components cover that page so it reads like the rest of the admin: a first-run hero, a setup checklist, toned callouts, a connection card, a feature grid, and a themable icon set.
Hero. The first-run panel before anything is configured: tb-hero centers an icon well, a headline, one supporting sentence, and the primary call to action.
<section class="tb-hero">
<span class="tb-hero-icon"><span class="tb-icon tb-icon-plug tb-icon-lg" aria-hidden="true"></span></span>
<h2 class="tb-hero-title">Connect your account</h2>
<p class="tb-hero-text">Link your advertising account to publish products and measure results without leaving Tringify.</p>
<div class="tb-hero-actions">
<button class="tb-btn">Connect account</button>
<button class="tb-btn tb-btn-ghost">Learn more</button>
</div>
</section>
Setup steps. tb-steps numbers itself; each tb-step declares data-state as done, current, todo, or off. A done step's marker draws a check, the current step's marker is emphasized, and an off step dims. Put the step's action button in tb-step-action.
<div class="tb-steps">
<article class="tb-step" data-state="done">
<span class="tb-step-marker" aria-hidden="true"></span>
<div class="tb-step-body">
<h3 class="tb-step-title">Connect your account</h3>
<p class="tb-step-text">Credentials are saved.</p>
</div>
<div class="tb-step-action"><button class="tb-btn tb-btn-sm tb-btn-ghost">Change</button></div>
</article>
<article class="tb-step" data-state="current">
<span class="tb-step-marker" aria-hidden="true"></span>
<div class="tb-step-body">
<h3 class="tb-step-title">Publish products</h3>
<p class="tb-step-text">Select this channel from each product's sales channels.</p>
</div>
<div class="tb-step-action"><button class="tb-btn tb-btn-sm tb-btn-outline">Open products</button></div>
</article>
<article class="tb-step" data-state="off">
<span class="tb-step-marker" aria-hidden="true"></span>
<div class="tb-step-body">
<h3 class="tb-step-title">Enable customer activity</h3>
<p class="tb-step-text">Available after products are published.</p>
</div>
</article>
</div>
Callouts. Use tb-callout-info for non-actionable context; it is intentionally neutral so ordinary guidance does not look selected or branded. Reserve tb-callout-success for a completed positive state and tb-callout-warning for a consequence that needs attention. Lead with a matching tb-icon when the message deserves one.
<div class="tb-stack">
<div class="tb-callout tb-callout-info"><span class="tb-icon tb-icon-info" aria-hidden="true"></span><p class="tb-callout-title">Feeds regenerate daily</p><p class="tb-callout-text">Product changes appear in the channel within 24 hours.</p></div>
<div class="tb-callout tb-callout-success"><span class="tb-icon tb-icon-check" aria-hidden="true"></span><p class="tb-callout-title">Catalog synced</p><p class="tb-callout-text">1,204 products are live on this channel.</p></div>
<div class="tb-callout tb-callout-warning"><span class="tb-icon tb-icon-warning" aria-hidden="true"></span><p class="tb-callout-title">3 products were skipped</p><p class="tb-callout-text">They are missing a required identifier. Review the activity log.</p></div>
</div>
Connect card. The connection status block: a mark well (an tb-icon or an <img>), the connected identity, a status line, and the connect or disconnect action. Set data-connected="true" to turn the status dot green.
<div class="tb-connect-card" data-connected="true">
<span class="tb-connect-mark"><span class="tb-icon tb-icon-globe tb-icon-lg" aria-hidden="true"></span></span>
<div class="tb-connect-body">
<p class="tb-connect-name">Aurora Living Ads</p>
<span class="tb-connect-status">Connected</span>
</div>
<div class="tb-connect-action"><button class="tb-btn tb-btn-sm tb-btn-outline">Disconnect</button></div>
</div>
Feature grid. "What this app does" tiles — a drawn icon, a short title, one supporting sentence. Use it instead of stat tiles when the content is capabilities, not metrics.
<div class="tb-feature-grid">
<div class="tb-feature"><span class="tb-icon tb-icon-box" aria-hidden="true"></span><h3 class="tb-feature-title">Product feeds</h3><p class="tb-feature-text">Publish your catalog with reviewed field mappings.</p></div>
<div class="tb-feature"><span class="tb-icon tb-icon-chart" aria-hidden="true"></span><h3 class="tb-feature-title">Conversion tracking</h3><p class="tb-feature-text">Measure results with server-side events.</p></div>
<div class="tb-feature"><span class="tb-icon tb-icon-spark" aria-hidden="true"></span><h3 class="tb-feature-title">Automatic updates</h3><p class="tb-feature-text">Price and stock changes sync on their own.</p></div>
</div>
Icons. tb-icon glyphs are CSS masks painted with currentColor, so they inherit text color and follow the theme. Size them with font-size or tb-icon-lg. Mark decorative icons aria-hidden="true".
<div class="tb-row" style="gap:16px;font-size:18px">
<span class="tb-icon tb-icon-check" title="check"></span>
<span class="tb-icon tb-icon-plug" title="plug"></span>
<span class="tb-icon tb-icon-spark" title="spark"></span>
<span class="tb-icon tb-icon-warning" title="warning"></span>
<span class="tb-icon tb-icon-info" title="info"></span>
<span class="tb-icon tb-icon-external" title="external"></span>
<span class="tb-icon tb-icon-arrow-right" title="arrow-right"></span>
<span class="tb-icon tb-icon-globe" title="globe"></span>
<span class="tb-icon tb-icon-box" title="box"></span>
<span class="tb-icon tb-icon-chart" title="chart"></span>
</div>
Class |
Glyph |
|---|---|
|
Checkmark — completed states |
|
Plug — connections and integrations |
|
Spark — automation and highlights |
|
Warning triangle |
|
Information circle |
|
External link |
|
Right arrow — forward actions |
|
Globe — channels and web |
|
Package — products and catalog |
|
Bar chart — reporting |
Empty states. tb-empty on its own is a single line of muted text. When the merchant can act on the emptiness, compose the optional parts instead: tb-empty-title names what is missing, tb-empty-text explains the consequence, and tb-empty-actions holds the button that resolves it. Use the plain one-line form only when there is genuinely nothing to do — a filtered list with no matches, for example.
<div class="tb-empty">
<p class="tb-empty-title">No pickup locations mapped</p>
<p class="tb-empty-text">Every fulfillment location needs a pickup location before shipments can be booked.</p>
<div class="tb-empty-actions"><button class="tb-btn tb-btn-sm">Create mapping</button></div>
</div>
One-time secrets. tb-secret shows a value the merchant can never see again — an API key or token at the moment it is minted. It is deliberately not an input: the value cannot be edited, it wraps instead of truncating so nothing is copied half-selected, and it is monospaced so 0/O and 1/l are distinguishable. Pair it with a copy control, because hand-transcribing a key is where the mistakes happen. Put it inside a tb-callout-warning and let the callout carry the "you will not see this again" framing — tb-secret renders the value, not the warning. Write the value with textContent, never value, and clear it when the merchant dismisses the callout so a re-render cannot bring it back.
<div class="tb-callout tb-callout-warning">
<span class="tb-icon tb-icon-warning" aria-hidden="true"></span>
<div class="tb-callout-text">
<p class="tb-callout-title">Copy this key now — it will not be shown again</p>
<p>Store it in your server's secret manager. If it is lost, revoke this key and create another.</p>
<div class="tb-secret">
<code class="tb-secret-value">cpk_live_7f3a9c21b84e05d6a1f7c9e2b4d80a35</code>
<button class="tb-btn tb-btn-sm tb-btn-outline tb-secret-copy">Copy</button>
</div>
</div>
</div>
Class |
Element |
Notes |
|---|---|---|
|
Wrapper |
Row on desktop, stacks below 600px |
|
|
Monospaced, wraps, |
|
Button |
Keeps its width when the value wraps; use with |
Collapsed explanations. Use native <details class="tb-disclosure"> for supporting information that is useful on demand but should not compete with the page's primary form. Keep it collapsed by omitting open. The browser owns keyboard interaction and disclosure state; no app JavaScript is needed. Put the title and one-line preview in tb-disclosure-summary, and the expanded material in tb-disclosure-content. For a short ordered workflow, use tb-disclosure-list; it numbers itself and keeps each item compact. Do not use a disclosure to hide required fields, errors, or the current status.
<details class="tb-disclosure">
<summary class="tb-disclosure-summary">
<span class="tb-disclosure-title">How orders are processed</span>
<span class="tb-disclosure-description">Creation is automatic; later stages follow your settings.</span>
</summary>
<div class="tb-disclosure-content">
<ol class="tb-disclosure-list">
<li><div><span class="tb-disclosure-item-title">Create order</span><span class="tb-disclosure-item-text">Always automatic.</span></div></li>
<li><div><span class="tb-disclosure-item-title">Book shipment</span><span class="tb-disclosure-item-text">Automatic or manual.</span></div></li>
</ol>
</div>
</details>
Class |
Element |
Purpose |
|---|---|---|
|
|
Framed disclosure; collapsed unless |
|
|
Clickable title and preview with a kit-drawn chevron |
|
|
Short disclosure heading |
|
|
One-line preview visible while collapsed |
|
|
Expanded content wrapper |
|
|
Optional compact, automatically numbered process list |
|
|
Process item heading and supporting text |
Disclosures inside a frame. Add tb-disclosure-flush when a disclosure sits inside a card, a settings section or another disclosure. It drops the frame and keeps one top rule, so the region is boxed once. Use one level of framing per region: never put a tb-card inside a disclosure; place the content straight in tb-disclosure-content instead.
<section class="tb-card">
<div class="tb-card-header"><h3 class="tb-h3">Count sales</h3></div>
<div class="tb-card-body tb-flow">
<p class="tb-muted">Sales are counted in Purchase (Tringify).</p>
<details class="tb-disclosure tb-disclosure-flush">
<summary class="tb-disclosure-summary">
<span class="tb-disclosure-title">Google tag</span>
<span class="tb-disclosure-description">Set up for you</span>
</summary>
<div class="tb-disclosure-content">
<dl class="tb-deflist"><dt>Tag ID</dt><dd><code>AW-123456789</code></dd></dl>
</div>
</details>
</div>
</section>
Where the disclosure sits |
Class |
|---|---|
Directly on the page |
|
Inside a card, settings section or disclosure |
|
Complete starter page#
Inspect layouts. Use tb-split when a detail page should show facts beside a preview. Put the readable record in tb-split-main and the preview in tb-split-aside. Below 720px the columns stack. Keep headline counts in tb-stat-grid; put heterogeneous term → value data in tb-deflist (values wrap); put a homogeneous stack of records in tb-rowlist. For a read-only row, tb-rowlist-primary is the fact and tb-rowlist-meta is the secondary detail. Reach for a settings-row summary inside a row list only when the row also carries an action.
<div class="tb-split">
<div class="tb-split-main">
<div class="tb-card">
<div class="tb-card-header"><h3 class="tb-card-title">Details</h3></div>
<div class="tb-card-body">
<dl class="tb-deflist">
<dt>Output</dt><dd>1164 × 6468 px</dd>
<dt>Print DPI</dt><dd>300</dd>
</dl>
</div>
</div>
<div class="tb-rowlist">
<div class="tb-rowlist-item"><span class="tb-rowlist-primary">826 × 1242 px</span><span class="tb-rowlist-meta">at 169, 39</span></div>
<div class="tb-rowlist-item"><span class="tb-rowlist-primary">826 × 1242 px</span><span class="tb-rowlist-meta">at 169, 1331</span></div>
</div>
</div>
<div class="tb-split-aside">
<div class="tb-media-frame tb-media-frame-scroll"><img alt="Tall artwork" width="180" height="720" src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='720' viewBox='0 0 180 720'%3E%3Crect fill='%23e7e5e4' width='180' height='720'/%3E%3Ctext x='90' y='360' text-anchor='middle' fill='%2378716c' font-size='14' font-family='system-ui,sans-serif'%3EArtwork%3C/text%3E%3C/svg%3E"></div>
</div>
</div>
Scrollable previews. tb-media-frame letterboxes an image inside a 420px cap. Add tb-media-frame-scroll when the asset is much taller or wider than that cap — proofs, banners, print plates — so the preview keeps its aspect and the frame scrolls instead of shrinking the image. Use this modifier when the preview needs to remain scrollable.
Class |
Element |
Purpose |
|---|---|---|
|
|
Two-column inspect layout; stacks below 720px |
|
|
Primary record column and preview column |
|
|
Read-only term → value; values wrap |
|
|
Direct children of a row-list item: fact and secondary detail |
|
modifier on |
Scroll a tall or wide preview instead of shrinking it |
A minimal embedded app page: the kit for appearance, the SDK for boot, theme, and session.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Products</title>
<link rel="stylesheet" href="https://bridge.tringify.com/app-bridge-ui@1.css">
</head>
<body>
<main class="tb-page">
<header class="tb-page-header"><h1 class="tb-h1">Products</h1></header>
<section class="tb-section">
<p id="status" role="status">Loading products…</p>
</section>
</main>
<script src="https://bridge.tringify.com/app-bridge@1.js"></script>
<script>
(async function () {
const status = document.getElementById("status");
try {
const bridge = TringifyBridge.init();
await bridge.ready();
const context = await bridge.getContext();
document.documentElement.dataset.theme = context.theme;
document.documentElement.lang = context.locale;
bridge.on("theme:change", (theme) => {
document.documentElement.dataset.theme = theme;
});
const { token } = await bridge.getSessionToken({ scopes: ["read_products"] });
const response = await fetch(
"https://api-store.tringify.com/api/2025-01/products?limit=20",
{ headers: { Authorization: `Bearer ${token}` } },
);
const body = await response.json();
if (!response.ok) {
throw new Error(body.error?.message || "Products could not be loaded");
}
const products = body.data.products;
status.textContent = products.length
? `Loaded ${products.length} products from the first page.`
: "There are no products to show.";
} catch (error) {
status.setAttribute("role", "alert");
status.textContent = "Products could not be loaded. Reopen the app or check its permissions.";
console.error("App startup failed", error);
}
})();
</script>
</body>
</html>
Accessible tabs and compact forms#
Use tb-tabs-scroll around a tb-tabs tab list and tb-tabpanel for its
panels. tb-section is for stacked sections; it adds separators between
sections and should not be used as the tab-panel layout.
<div class="tb-tabs-scroll">
<div class="tb-tabs" role="tablist" aria-label="Settings" id="settings-tabs">
<button class="tb-tab" type="button" role="tab" id="account-tab"
aria-controls="account-panel" aria-selected="true">Account</button>
<button class="tb-tab" type="button" role="tab" id="messages-tab"
aria-controls="messages-panel" aria-selected="false">Messages</button>
</div>
</div>
<section class="tb-tabpanel" role="tabpanel" id="account-panel"
aria-labelledby="account-tab" tabindex="0">Account settings</section>
<section class="tb-tabpanel" role="tabpanel" id="messages-panel"
aria-labelledby="messages-tab" tabindex="0" hidden>Message settings</section>
<script>
const tabs = TringifyBridge.ui.enhanceTabs(document.getElementById('settings-tabs'));
// Optional: tabs.select('messages-tab'); tabs.sync(); tabs.destroy();
</script>
enhanceTabs(tablist, { onChange(tab) {} }) manages selection, hidden panels,
keyboard focus, arrow keys and Home/End. Horizontal lists follow text direction;
use aria-orientation="vertical" for vertical lists. Disabled tabs are skipped.
Give each tab and panel a unique ID. Initialize nested lists separately. Call
sync() after replacing tabs and destroy() before removing the component.
Use tb-input-action for an input and its action button, and tb-text-preserve
for readable message previews that retain line breaks. Use a fieldset.tb-fieldset to disable a related group of
fields without browser-default borders. A <legend> can name the group.
<div class="tb-field">
<label class="tb-label" for="template-id">Template ID</label>
<div class="tb-input-action">
<input class="tb-input" id="template-id">
<button type="button" class="tb-btn tb-btn-outline">Check template</button>
</div>
</div>
<p class="tb-text tb-text-preserve">Your message preview</p>
For selects, associate a <label> with the original <select>, add any
aria-describedby or validation attributes, then call ui.enhanceSelect(select).
The Kit connects the label and help text to the visible picker and hides the
native duplicate from assistive technology. Its destroy() method restores the
native field. Keep application validation and save/network behavior in the app;
use App Bridge's Save/Discard bar for unsaved settings.
Form picker lifecycle and options#
Initialize each helper once after its input is in the document. Keep the native field for FormData, validation, values, and events. Labels and help text follow the visible control, disabled fieldsets disable it, and a native form reset restores its display. Call sync() after changing .value or .files programmatically; property assignments do not emit browser events. Call destroy() before unmounting to remove listeners and restore the original field. Select and date handles also provide close(). Required fields expose their invalid state on the visible control when native validation runs; display any app-specific error message beside the field.
Helper |
Options |
Notes |
|---|---|---|
|
No options object |
One local choice; groups and keyboard typeahead. |
|
|
|
|
|
|
All form and navigation helpers#
Helper |
Purpose |
Lifecycle |
|---|---|---|
|
One choice from local HTML options, with grouping and keyboard typeahead. |
|
|
One calendar day using native date values and bounds. |
|
|
Local file selection and accepted drops; does not upload. |
|
|
App-owned tab selection, keyboard focus, and panels. |
|
|
One counter linked to a field declaring maxlength or data-maxlength. |
|
|
Adds counters to declared fields; skips already enhanced fields and data-charcount="off". |
Returns newly created counter handles. |
Counters follow input and change events, form resets, and changed limits. Their count matches native maxlength behavior, which counts UTF-16 code units; some symbols use more than one unit. Call sync() after assigning a field value in code and destroy() before removing the control. Tabs and counters are UI helpers, not resource pickers. Provider accounts, pickup mappings, feed fields, and provider-approved message templates are app-owned controls; the host does not supply custom options for them.
Form pages#
Use tb-form-page for create and edit screens. It matches Store Admin's Product form: section cards in tb-form-page-main (two thirds) beside settings cards in tb-form-page-aside (one third), stacking into one column below 1024px. Build each card as tb-card tb-form-section; its body spaces fields 16px apart. Pair fields with tb-form-row, or use tb-form-row-3 and tb-form-row-4 for short numeric fields. Wrap a repeatable item, such as a placement area, in tb-form-item with a tb-form-item-header for its title and Remove action. Mark required labels with <span class="tb-required" aria-hidden="true">*</span>. Put Cancel and Save in the header's tb-header-actions, and add tb-header-sticky so they stay visible while the form scrolls.
<header class="tb-header tb-header-sticky">
<button class="tb-header-back" type="button" aria-label="Back"></button>
<div class="tb-header-titles"><h2 class="tb-header-title">New template</h2></div>
<div class="tb-header-actions">
<button class="tb-btn tb-btn-sm tb-btn-outline" type="button">Cancel</button>
<button class="tb-btn tb-btn-sm" type="button">Save</button>
</div>
</header>
<div class="tb-form-page">
<div class="tb-form-page-main">
<section class="tb-card tb-form-section">
<div class="tb-card-header"><h3 class="tb-card-title">Template</h3></div>
<div class="tb-card-body">
<div class="tb-field"><label class="tb-label" for="name">Name<span class="tb-required" aria-hidden="true">*</span></label><input id="name" class="tb-input"></div>
<div class="tb-form-row">…</div>
</div>
</section>
</div>
<div class="tb-form-page-aside">
<section class="tb-card tb-form-section">…</section>
</div>
</div>
Settings pages#
For app settings, follow Store Admin's Settings > General: stack section cards in tb-settings-page, add tb-form-section-loose so fields sit 24px apart, and finish with one tb-form-footer holding the Save button at the bottom right. Keep one topic per card and pair related numbers with tb-form-row.
Wrap a checkbox in <label class="tb-check"> to get Store Admin's checkbox and a label row; use tb-switch for settings that take effect as a toggle. Date inputs enhanced with TringifyBridge.ui.enhanceDate use Store Admin's calendar. When a date is optional, offer common choices in a select (for example 30 days, 90 days, 1 year) and show the calendar only for a custom date.
Typography and Settings spacing#
UI Kit 1.0.72 uses Inter to match Store Admin. The font is served from bridge.tringify.com; if your app has a Content Security Policy, allow font-src https://bridge.tringify.com. A system font is used when Inter cannot load. For Settings, wrap section cards in tb-settings-page and add tb-form-section-loose to each tb-card tb-form-section. This gives fields 24px of separation and labels 8px before their controls. Place one Save action in tb-form-footer below the cards. Use tb-callout-info for a quiet informational note; warning and success messages keep their distinct treatments.
Dashboard statistics#
Use tb-stat-strip for a small set of headline counts. Each tb-stat-tile has a white card surface and can include a decorative tb-stat-icon. Use any UI Kit tb-icon inside it. Add data-tone="green" or data-tone="violet" to change the icon color; the default is blue. Keep a visible label with each value.
<div class="tb-stat-strip">
<div class="tb-stat-tile">
<span class="tb-stat-icon" aria-hidden="true"><span class="tb-icon tb-icon-box"></span></span>
<strong class="tb-stat-value">53</strong>
<span class="tb-stat-label">Templates</span>
</div>
<div class="tb-stat-tile" data-tone="green">
<span class="tb-stat-icon" aria-hidden="true"><span class="tb-icon tb-icon-check"></span></span>
<strong class="tb-stat-value">48</strong>
<span class="tb-stat-label">Published</span>
</div>
</div>
Dashboard banners#
Place tb-banner elements in tb-banner-grid. Set data-size to square (or 1x1), rectangle (or 2x1), 2x2, 3x1, or 3x2. These sizes describe column-by-row spans on a wide three-column layout; banners become full-width cards on narrow screens. Use data-tone="ink" for a dark plain banner. You can also set --tb-banner-bg and --tb-banner-fg on an individual banner to match your app.'s palette.
For an image banner, add tb-banner--image and an <img class="tb-banner-image">; the Kit adds a text-legibility gradient. Put the eyebrow, title, supporting text, and optional icon inside tb-banner-content. A direct tb-banner-link makes the full banner clickable. Give the link an accessible name, and put any separate buttons in tb-banner-actions rather than inside that full-banner link. Keep essential words as HTML text, not baked into the image.
<div class="tb-banner-grid">
<article class="tb-banner tb-banner--image" data-size="2x1">
<img class="tb-banner-image" src="/banner.jpg" alt="">
<a class="tb-banner-link" href="/templates" aria-label="Browse templates"></a>
<div class="tb-banner-content">
<p class="tb-banner-eyebrow">Get started</p>
<h3 class="tb-banner-title">Make it yours</h3>
<p class="tb-banner-text">Choose a template and customize the artwork.</p>
<div class="tb-banner-actions">
<a class="tb-btn tb-btn-outline" href="/templates/new">Create template</a>
</div>
</div>
</article>
<article class="tb-banner" data-size="1x1" data-tone="ink">
<div class="tb-banner-content">
<span class="tb-banner-icon" aria-hidden="true"><span class="tb-icon tb-icon-box"></span></span>
<h3 class="tb-banner-title">Ready to print?</h3>
</div>
</article>
</div>
Setup steps#
UI Kit 1.0.73 adds tb-steps-cards for a short onboarding sequence. Put it alongside tb-steps; each tb-step contains a tb-step-marker, tb-step-body, and optional tb-step-action. Set data-state to current, done, todo, or off. The marker shows the step number or a completion check, and the cards stack on narrow screens. Use tb-steps without the card class when a compact list fits better. Give action buttons descriptive labels.
<div class="tb-steps tb-steps-cards">
<article class="tb-step" data-state="done">
<span class="tb-step-marker" aria-hidden="true"></span>
<div class="tb-step-body">
<h3 class="tb-step-title">Publish a template</h3>
<p class="tb-step-text">Your artwork is ready to use.</p>
</div>
<div class="tb-step-action">
<a class="tb-btn tb-btn-sm tb-btn-ghost" href="/templates">View templates</a>
</div>
</article>
<article class="tb-step" data-state="current">
<span class="tb-step-marker" aria-hidden="true"></span>
<div class="tb-step-body">
<h3 class="tb-step-title">Assign it to a product</h3>
<p class="tb-step-text">Choose where customers can customize.</p>
</div>
<div class="tb-step-action">
<a class="tb-btn tb-btn-sm tb-btn-outline" href="/products">Open products</a>
</div>
</article>
</div>
Include a step only when your app can tell when it is complete. After the sequence is finished, replace the checklist with useful management links. Keep optional Settings actions outside the completion sequence.
Counted fields and select placeholders#
UI Kit 1.0.74 places a counted field's help text and character count on one caption row when the input and tb-field-help are in the same tb-field. Call TringifyBridge.ui.enhanceCharCounts(document) after the fields exist. If you set an input's .value in code, call the returned counter handle's sync(); assigning .value does not fire an input event. For an enhanced select, use <option value="" disabled selected>Choose…</option> for a prompt that cannot be selected from the menu. Leave an empty option enabled only when it represents a real choice such as “None”.
To select existing store records, open a host resource picker with bridge.resourcePicker.open(); the UI Kit does not draw its rows or page controls. Product, Brand, Vendor, Category, Collection, Product Attribute, and Product Tags pickers use their catalog icons, show the current page, and offer page-size choices. A search or page-size change starts again on page 1, and selections remain chosen across pages. See Dialogs and pickers for the picker contract.
Filter toolbars and date ranges#
Put a list's search and its filters in one tb-toolbar: a tb-search, then one tb-toolbar-group per set of related filters. From UI Kit 1.0.76 a group keeps its controls side by side and, when the row runs out of room, wraps under the search as a whole, so selects never stack into a narrow column. Enhanced selects and dates (ui.enhanceSelect, ui.enhanceDate) size to their content inside a toolbar. For a date filter use tb-date-range: one bordered control with an optional label, the two date inputs and a dash. Enhance each input with ui.enhanceDate as usual; inside a range the pickers lose their own border. On narrow screens each group takes the full width.
<div class="tb-toolbar">
<div class="tb-search"><input class="tb-input" type="search" placeholder="Search orders" aria-label="Search orders"></div>
<div class="tb-toolbar-group">
<select class="tb-input" aria-label="Status">…</select>
<select class="tb-input" aria-label="Sort">…</select>
</div>
<div class="tb-toolbar-group">
<div class="tb-date-range">
<span class="tb-date-range-label">Created</span>
<input id="from" type="date" aria-label="Created from">
<span class="tb-date-range-sep" aria-hidden="true">–</span>
<input id="to" type="date" aria-label="Created to">
</div>
<button class="tb-btn tb-btn-sm tb-btn-ghost" type="button">Clear filters</button>
</div>
</div>
Link to Tringify-hosted Pricing#
Style the action with UI Kit's tb-btn tb-btn-outline classes. After await bridge.ready(), hide the button if !bridge.supports('navigate'); otherwise call bridge.billing.openPricing() from its click handler and handle promise errors through your normal app feedback. The host checks unsaved changes before leaving the app. See App Bridge getting started and UI Kit.
Welcome banner and setup progress#
Most merchants are not technical. Open your app's home page by saying what the app does for them, then show the next thing to do. UI Kit 1.0.77 adds tb-hero-feature for that welcome banner: an eyebrow (tb-hero-eyebrow), a headline, one or two sentences in everyday words and the next action, inside tb-hero-body, beside one piece of artwork (<img class="tb-hero-art" alt="">). On narrow screens the artwork moves above the copy. Use artwork without text or currency symbols, so it reads the same in every language and country.
<section class="tb-hero tb-hero-feature">
<div class="tb-hero-body">
<p class="tb-hero-eyebrow">Sell on Google</p>
<h2 class="tb-hero-title">Show your products to people searching on Google</h2>
<p class="tb-hero-text">Set it up once. Prices and stock stay up to date by themselves.</p>
<div class="tb-hero-actions"><button class="tb-btn">Start setup</button></div>
</div>
<img class="tb-hero-art" src="/art/hero.webp" alt="">
</section>
Below the banner, show how far the merchant has come with tb-progress, then the steps (tb-steps tb-steps-cards). Set --tb-progress to the percentage done and keep the aria-valuenow and aria-valuemax values in step with it. From 1.0.77 the step to do next is marked in ink rather than color: its card gets a dark border and a filled marker, and finished steps sit on a muted background. Mark exactly one step current. For each step, say why it matters in one sentence and make its button name the action, such as "Connect Google" rather than "Continue".
<div class="tb-progress" role="progressbar" aria-label="Setup progress"
aria-valuemin="0" aria-valuemax="5" aria-valuenow="2" style="--tb-progress: 40%">
<div class="tb-progress-header">
<p class="tb-progress-label">Your setup</p>
<span class="tb-progress-value">2 of 5 done</span>
</div>
<div class="tb-progress-track"><div class="tb-progress-bar"></div></div>
</div>
Guide merchants through tasks that happen outside your app, such as finding an ID in another service, with a tb-disclosure holding a numbered tb-disclosure-list: one short title (tb-disclosure-item-title) and one sentence (tb-disclosure-item-text) per step. Put context that is not a step, such as why the task matters, in a paragraph above the list rather than numbering it. Name things the way the other service names them on screen. From UI Kit 1.0.80 the step text always sits beside its number; earlier versions could wrap it into the number column. For text only screen readers should hear, such as a table caption or the name of an icon-only button, use tb-sr-only.
Page spacing#
Put tb-flow on a view's content (UI Kit 1.0.78): every block after the first gets the same 16px gap, whatever it is, so a callout above a list or a disclosure above a table never touches. Use tb-flow-loose to separate whole sections (40px, 32px on phones), and group a heading with its description in a <header> so they stay together. Prefer it to margins on individual components.
<div class="tb-view tb-flow-loose">
<section class="tb-hero tb-hero-feature">…</section>
<section class="tb-flow">
<header><h2 class="tb-h2">Get set up</h2><p class="tb-section-description">Five short steps.</p></header>
<div class="tb-progress">…</div>
<div class="tb-steps tb-steps-cards">…</div>
</section>
</div>
Long lists in selects#
From App Bridge 1.0.62, ui.enhanceSelect adds a search box to the top of the menu when a select has more than 10 options, such as a list of countries or languages. Typing filters the options and ignores accents, so "cote" finds Côte d'Ivoire; Arrow Down moves into the list and Enter picks the first match. Set data-search="true" or data-search="false" on the select to override the length rule. Order long lists for people: put the most likely choice first, such as the store's own country, then sort the rest by name rather than by code. Search inputs show an ink clear button instead of the browser's blue one (UI Kit 1.0.80).