@ambasada/ui
The Ambasada look for mini apps: design tokens (the Ambasada colours, Inter + JetBrains Mono, radii, spacing,
the mono label, the shell's safe areas and host bar) and a small component set in plain CSS, with optional
React wrappers. Light only, no framework, no runtime: one stylesheet a plain HTML page can <link>.
Install
npm install @ambasada/ui
import "@ambasada/ui"; // fonts + tokens + components, once at the root of the app
Or the parts: @ambasada/ui/tokens.css, @ambasada/ui/components.css, @ambasada/ui/fonts.css (skip
fonts.css if you serve Inter and JetBrains Mono yourself).
With no build step, the same three in one file from any npm CDN (the fonts load from next to it):
<link rel="stylesheet" href="https://unpkg.com/@ambasada/[email protected]/dist/ambasada-ui.css" />
Optional: the ambasada init templates start unstyled, and only the mini app's in-app page (ambasada miniapp enable) loads it. Use it on your own pages if you want the Ambasada app's look.
Tokens
All on :root, from the Ambasada hub (each has a source comment in tokens.css): --color-accent (the logo
red), --color-accent-text (red for small text), --color-bg, --color-surface, --color-surface-2,
--color-ink / -2 / -3, --color-line, --color-success / warn / danger / info and their -soft
tints, --card-* (the member card palettes), --font-sans, --font-mono, --text-*, --weight-*,
--tracking-label, --radius-*, --shadow-*, --space-1 to --space-10, --page-max, --tap, --ease,
--duration.
The shell's insets, for anything you position yourself:
| Variable | Default | Inside the Ambasada app |
|---|---|---|
--ambasada-safe-area-top / bottom / left / right | env(safe-area-inset-*, 0px) | set on <html> by @ambasada/miniapp's ready() |
--ambasada-host-bar | 0px | the height of the shell's top bar |
--ambasada-inset-top / bottom / left / right | max(--ambasada-safe-area-*, env(safe-area-inset-*)) | what to keep clear: use these |
Use the --ambasada-inset-* tokens for anything you place yourself. They take the larger of the shell's report and
the browser's own inset, so the inset is right inside the app and on the open web and is never counted twice. Keep
the margin on top of it small (8 px): an empty band above the home indicator is a bug.
.my-bar { position: fixed; bottom: 0; padding-bottom: calc(8px + var(--ambasada-inset-bottom)); }
Components
| Class | What |
|---|---|
ui-page | The 480 px column, padded clear of the host bar and every safe area. |
ui-card, ui-card-title | White card, hairline border, 16 px radius. |
ui-button, ui-button secondary, block | Primary (brand red: one per screen) and secondary buttons, ≥ 44 px. On <button> or <a>. |
ui-label | The mono label: 11 px, uppercase, 0.1em. Captions, never sentences. |
ui-group (ui-group-head, ui-group-body) | A list: rows in one bordered block, with a title. |
ui-row (row-icon, row-text, row-title, row-sub, row-right, row-chev) | List row; tappable, danger, muted; row-icon accent / good / bad. |
ui-pill | Status pill with a dot: good, bad, warn, accent, pulse. |
ui-sheet-backdrop > ui-sheet (ui-sheet-handle > ui-sheet-grip + ui-sheet-head, -body, -foot), ui-icon-btn | Bottom sheet at every width (no centred dialogs): grip, rounded top, dimmed page, a body that scrolls, the one primary action in the foot. Add ui-sheet-open to <body> while open. The React Sheet also portals to <body>, closes on a drag down, and keeps the foot above the keyboard. |
ui-field (ui-field-label, ui-field-hint, .bad/.good), ui-input | A form field: label, control, one line of hint or error under it, 8 px apart; compact inside ui-shell with a mouse. ui-input on <input>, <textarea>, <select> (mono, code). |
ui-stepper-wrap > ui-stepper (ui-stepper-value), ui-presets | − value + for amounts, with preset chips (on). |
ui-empty (ui-empty-icon) | Empty state: icon, <strong> title, hint, action. |
ui-skeleton | Grey bars (<span>s) while loading: skeletons, not spinners. |
For tools built for the laptop that still open in the Ambasada app (the Developer app):
| Class | What |
|---|---|
ui-shell > ui-shell-side, ui-shell-scrim, ui-shell-main > ui-shell-top (ui-shell-menu), ui-shell-content | 240 px sidebar, sticky top bar, content up to 1088 px. Under 900 px the sidebar is a drawer (ui-shell open). Clears the host bar and safe areas once. |
ui-nav (ui-nav-title), ui-nav-item (ui-nav-label, ui-nav-right, on) | Sidebar navigation; the current item gets a red marker. |
ui-tabs | Tabs as links under a title (a.on); scrolls sideways when narrow. |
ui-field, ui-input | The same field as above, compact here with a mouse (40 px, 14 px text). <Field label value onChange /> renders the input itself. |
ui-table (.num, tr.tappable) | Data table: hairline rows, tabular figures. |
<main class="ui-page">
<p class="ui-label">// My Company</p>
<section class="ui-card">
<h2 class="ui-card-title">Your name as a folk poem</h2>
<button class="ui-button block">Pay 2 TON</button>
</section>
<section class="ui-group">
<div class="ui-group-head"><h2>Orders</h2></div>
<div class="ui-group-body">
<a class="ui-row tappable" href="/orders/1">
<span class="row-text"><span class="row-title">Poem for Ana</span><span class="row-sub">2 min ago</span></span>
<span class="row-right"><span class="ui-pill good"><i></i>Ready</span></span>
</a>
</div>
</section>
</main>
React
import { Button, Card, Empty, Field, Group, Nav, NavItem, Page, Pill, Row, Shell, Sheet, Skeleton, Stepper, Tabs } from "@ambasada/ui/react";
<Page>
<Card title="Your name as a folk poem">
<Button block onClick={() => setOpen(true)}>Pay 2 TON</Button>
</Card>
<Group title="Orders">
<Row title="Poem for Ana" sub="2 min ago" right={<Pill tone="good">Ready</Pill>} href="/orders/1" />
</Group>
<Sheet open={open} onClose={() => setOpen(false)} title="Pay" footer={<Button>Pay 2 TON</Button>}>
<Stepper value={amount} onChange={setAmount} min={1} unit="TON" presets={[1, 2, 5]} />
</Sheet>
</Page>
<Shell sidebar={<Nav title="Apps"><NavItem label="Acme" href="/p/acme" current /></Nav>} top="Acme" open={menu} onMenu={setMenu}>
<Tabs tabs={[{ id: "overview", label: "Overview", href: "/p/acme" }]} current="overview" />
<Field label="Slug" value={slug} onChange={(e) => setSlug(e.target.value)} hint="acme.ambasada.dev" />
</Shell>
The wrappers render markup only and import no CSS (load the stylesheet yourself). The module is marked
"use client" (Sheet uses effects), so Next.js server components can render them too; they server-render
with react-dom/server. React ≥ 18 is an optional peer dependency.
Rules for the look (one red thing per screen, light only, sheets for forms): docs/DESIGN-SYSTEM.md and docs/MINI-APPS.md §9 in the Ambasada repo.
MIT, © CENTYSO. Inter and JetBrains Mono: SIL Open Font License 1.1 (fonts/OFL-*.txt).