Styling your app

Use the App Bridge UI kit so your embedded page looks native in Storeadmin.

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.

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

app.js javascript
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

--tb-bg / --tb-fg

Page background and foreground text

--tb-card

Card surfaces

--tb-muted / --tb-muted-fg

Muted surfaces and secondary text

--tb-border

Borders and dividers

--tb-primary / --tb-primary-fg

Primary buttons and solid badges

--tb-accent

Hover and selected surfaces

--tb-ring

Focus rings

--tb-radius

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

tb-h2, tb-card-title

24px / 600

Card or section title

tb-h3, tb-fieldset-title

16px / 600

Compact section heading

tb-label

14px / 500

Form label

tb-section-description, tb-card-description

14px / 400

Supporting description

tb-input, tb-btn

14px

Form control or action

tb-field-help

12px / 400

Form help text

tb-link

14px / 550

Inline link

tb-mono

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.

Page scaffold

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

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.

Settings rows

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.

Forms

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.

Card form

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

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

app.js javascript
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.

app.js javascript
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.

app.js javascript
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.

app.js javascript
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.

Buttons

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.

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

Data display

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.

Preview

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.

Preview

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.

Preview

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.

Tabs

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.

Skeleton loading

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.

States

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.

Preview

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.

Preview

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.

Preview

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.

Preview

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.

Preview

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

Preview

Class

Glyph

tb-icon-check

Checkmark — completed states

tb-icon-plug

Plug — connections and integrations

tb-icon-spark

Spark — automation and highlights

tb-icon-warning

Warning triangle

tb-icon-info

Information circle

tb-icon-external

External link

tb-icon-arrow-right

Right arrow — forward actions

tb-icon-globe

Globe — channels and web

tb-icon-box

Package — products and catalog

tb-icon-chart

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.

Preview

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.

Preview

Class

Element

Notes

tb-secret

Wrapper

Row on desktop, stacks below 600px

tb-secret-value

<code>

Monospaced, wraps, user-select: all so one click selects the whole key

tb-secret-copy

Button

Keeps its width when the value wraps; use with tb-btn tb-btn-sm tb-btn-outline

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.

Preview

Class

Element

Purpose

tb-disclosure

<details>

Framed disclosure; collapsed unless open is present

tb-disclosure-summary

<summary>

Clickable title and preview with a kit-drawn chevron

tb-disclosure-title

<span>

Short disclosure heading

tb-disclosure-description

<span>

One-line preview visible while collapsed

tb-disclosure-content

<div>

Expanded content wrapper

tb-disclosure-list

<ol>

Optional compact, automatically numbered process list

tb-disclosure-item-title / tb-disclosure-item-text

<span>

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.

Preview

Where the disclosure sits

Class

Directly on the page

tb-disclosure

Inside a card, settings section or disclosure

tb-disclosure tb-disclosure-flush

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.

Preview

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

tb-split

<div>

Two-column inspect layout; stacks below 720px

tb-split-main / tb-split-aside

<div>

Primary record column and preview column

tb-deflist

<dl>

Read-only term → value; values wrap

tb-rowlist-primary / tb-rowlist-meta

<span>

Direct children of a row-list item: fact and secondary detail

tb-media-frame-scroll

modifier on tb-media-frame

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.

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

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

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

enhanceSelect(input)

No options object

One local choice; groups and keyboard typeahead.

enhanceDate(input, options)

locale, weekStartsOn, label, placeholder, previousMonthLabel, nextMonthLabel, todayLabel, clearLabel

weekStartsOn is 0–6. Localize action labels when your app uses another language.

enhanceFile(input, options)

cta, placeholder, hint, clearLabel, multipleLabel, multipleError, typeError, onReject(error)

multipleLabel may contain {count}. Native multiple and accept control drop acceptance.

All form and navigation helpers#

Helper

Purpose

Lifecycle

enhanceSelect(select)

One choice from local HTML options, with grouping and keyboard typeahead.

sync(), close(), destroy()

enhanceDate(input, options)

One calendar day using native date values and bounds.

sync(), close(), destroy()

enhanceFile(input, options)

Local file selection and accepted drops; does not upload.

sync(), destroy()

enhanceTabs(tablist, options)

App-owned tab selection, keyboard focus, and panels.

select(idOrElement), sync(), destroy()

enhanceCharCount(input)

One counter linked to a field declaring maxlength or data-maxlength.

sync(), destroy(), element

enhanceCharCounts(root)

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.

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

index.html html

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

index.html html

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

index.html html

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

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

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.

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

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

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