Install
Example
Your manifest’sadmin.page, as plain HTML and TypeScript:
<form> around them like native ones:
new FormData(form) holds sender, delay and skipReturns.
The dashboard’s rules
The elements keep DESIGN.md, the dashboard’s design law. Your page keeps the rest:- One ink button per view.
variant="primary"is the dashboard’s dark call to action; put the view’s one primary in the page header’s actions. Everything else isoutline(the default),ghostordestructive. - Status is a
<tsy-status-pill>, never a coloured word or a badge of your own. - No new colour. Use the tokens (
var(--tsy-color-…)), never a hex code. Cyan (--tsy-color-brand-cyan) is a highlighter: selection, progress, charts; never a button or a status. - 14px is the floor for any text, and type is the dashboard’s sans.
- Logical CSS only (
margin-inline-start,padding-inline,text-align: start), so the same rules serve Arabic and English. - Money and dates read as the dashboard writes them:
9.000 دجin Arabic,DZD 9,000in English; datesyyyy-MM-dd(yyyy-MM-dd HH:mm). - Modals, toasts, resource pickers, the save bar and the page title are
not elements here. They are App Bridge calls (
modal.open,toast.show,resource.pick,saveBar.show,titleBar.set): the dashboard draws them, signed with your app’s name. See@tassyir/app-bridge.
Language, direction and theme
Your page runs in the dashboard’s frame and cannot see its settings: App Bridge sends them (context.changed: locale, dir, theme).
followDashboardContext(bridge) applies the current context at once and
every change after it, and returns the function that stops. It sets three
attributes on <html>:
lang:aroren. The elements’ own words follow it (“Back”, “Loading”, “Dismiss”, “(opens in a new tab)”: Arabic forar, English otherwise).dir:rtlorltr. Every element is drawn with logical properties; arrows and carets mirror; arrow keys in a radio group follow the reading direction.data-theme:lightordark. The tokens switch, withcolor-scheme.
applyDashboardContext({ locale, dir, theme }, root?) sets them yourself,
for example in a page opened outside the dashboard. Light is the default. A
subtree with its own data-theme keeps that theme inside a page of the other
one.
The helpers take @tassyir/app-bridge’s bridge as it is but do not import
it: any object with context.current and on('context.changed', …) works.
Elements
Importing@tassyir/ui defines every element (in a browser; on a server it
only exports, so server rendering can import it). An attribute that takes a
value ignores one it does not know and uses its default.
Frameworks: React 19, Vue, Svelte and Lit set a prop as a property when the
element has one and as an attribute otherwise; every property here that
shares an attribute’s name (value, checked, form, prefix, type, …)
takes the attribute’s value, so the markup above works as written. Events
are DOM events (input, change, tsy-dismiss): where a framework does
not bind them, add a listener with addEventListener (in React, through a
ref).
<tsy-button>
The dashboard’s button: 36px, 12px corners, a 14px medium label.
aria-label, aria-pressed and aria-expanded are passed to the inner
control. A submit or reset acts after every click listener ran, unless one
called preventDefault(). Slot: the label (a 16px <svg> may sit beside it).
<tsy-link>
An underlined link in the colour of the text around it, ink on hover.
aria-label passes to the inner <a>. In the dashboard, your app moving to
another of its pages is bridge.navigate({ path }); leaving for a dashboard
page is bridge.navigate({ dashboard }).
<tsy-text-field>
A labelled input, or a text area with multiline.
Properties:
value, form, validity, validationMessage; methods
select(), checkValidity(), reportValidity(), setCustomValidity().
Events: input and change, from the element.
<tsy-select>
A labelled native select: on a phone, the phone’s own picker opens. Write
its <option> and <optgroup> elements inside it.
The options are mirrored into the inner select whenever they change.
Properties:
value, options, selectedIndex, form, validity. Events:
input, change.
<tsy-checkbox>
A checkbox with its label as content.
aria-label names a checkbox without content (a table’s row selection).
Events: input, change.
<tsy-radio-group> and <tsy-radio>
A labelled set of choices. Arrow keys move and choose (mirrored right to
left); the group is one Tab stop.
<tsy-radio-group>:
<tsy-radio>:
Events (from the group):
input, change. Methods: checkValidity(),
reportValidity(), setCustomValidity(), focus() (the chosen radio, or
the first). A radio added or removed later is taken into account: a removed
choice clears the value.
<tsy-card>
The dashboard’s content card. With a heading it is the layered card: a
tinted header bar with the title in the quiet label style, an optional
count, actions at the end, over a paper body. Without, a plain paper card.
Slots: the body;
actions. Never put a card inside a card.
<tsy-page-header>
The page’s title, its one <h1>, with an optional back link, a muted line
of facts and the page’s actions.
Slots:
meta, actions. The dashboard’s title bar already shows your app’s
name; bridge.titleBar.set({ title }) adds the page’s title beside it.
<tsy-table>
The dashboard’s table frame around a native <table> you write inside it:
a tinted frame, an optional header bar, a toolbar strip, the rows as a paper
panel that scrolls sideways on a phone, and a footer. The table keeps its
native semantics.
Slots: the
<table>; actions, toolbar, footer. On cells:
data-align="end" aligns a number to the end, data-wrap lets a cell wrap.
A row with aria-selected="true" is tinted. The table’s styles are adopted
into the document (or the shadow root it lives in), in the tassyir-ui
cascade layer.
<tsy-status-pill>
The dashboard’s only status treatment: a dot, a 12% tint of the tone, the
words in the tone pushed toward ink.
Slot
icon: a 16px glyph in place of the dot (a lock, say).
<tsy-banner>
A quiet tinted notice: a glyph, an optional heading, the message and
actions.
Slots: the message;
actions. Closing fires tsy-dismiss (bubbles,
cancelable); unless a listener prevents it, the banner hides. dismiss()
does the same from code. A passing confirmation (“Saved”) is a toast:
bridge.toast.show.
<tsy-empty-state>
What a list says when it has nothing yet. Keep it for a surface with
something to teach; in a form, a plain button says more.
Slots:
media (a glyph), the sentence, actions.
<tsy-skeleton>
A loading placeholder in the shape of what is coming. The dashboard shows
skeletons, not spinners, while a page or a card loads.
It is hidden from assistive technology; mark the region it stands in
aria-busy="true" and clear it when the content arrives. The pulse stops
under reduced motion.
<tsy-spinner>
In-place loading: a button, a row.
Forms
<tsy-text-field>, <tsy-select>, <tsy-checkbox>, <tsy-radio-group> and
<tsy-button> are form-associated custom elements: they submit with the
native <form> around them (FormData, a normal submit), take part in its
validation (required, the input’s own constraints, setCustomValidity),
are reset with it and follow a disabled <fieldset>. Enter in a one-line
field does what it does in a native one: it clicks the form’s first submit
button (nothing happens while that button is disabled or loading, so a
save in flight is not sent twice); with no submit button, it submits a form
that has only that one field.
- Label every field with its
labelattribute. A<label for>outside the element does not reach the input inside it. errorshows a message and marks the field invalid for assistive technology, but does not block the form: it is for your server’s answer. To block, callsetCustomValidity(message).- In the dashboard, saving a form the merchant edits is the dashboard’s save
bar:
bridge.saveBar.show()and itssaveBar.save/saveBar.discardevents.
Tokens
Importing the package adopts the tokens into the document. Use them in your own CSS:- Colours, both themes:
--tsy-color-ink,-ink-secondary,-ink-tertiary,-paper,-paper-sunken,-muted-fill,-accent-fill,-hairline,-field,-on-ink,-skeleton,-brand-cyan,-success,-success-soft,-warning,-warning-ink,-warning-soft,-info,-info-soft,-destructive,-destructive-on,-destructive-soft,-purple,-pill-surface,-pill-ink,-control-hover,-ghost-hover,-destructive-fill; shadows--tsy-shadow-paper,--tsy-shadow-control. - Type:
--tsy-font-sans,--tsy-font-mono; sizes--tsy-font-size-body,-label(14px),-title(18px),-headline(20px),-page-title(24px),-page-title-rtl(22px); weights--tsy-font-weight-regular(400),-medium(500),-semibold(600),-bold(700, the page title only);--tsy-line-height-*. - Space:
--tsy-space-hint(6px),-hug(8px),-cluster(12px),-grid(16px),-section(24px). Radii:--tsy-radius-control(12px),-nested(8px),-pill.
@tassyir/ui/tokens (no DOM, safe on a server:
colors.dark.ink, breakpoints.sm, …) and a stylesheet in
@tassyir/ui/tokens.css for a page that wants the tokens without the
elements. They sit in the tassyir-ui cascade layer, with the page’s
defaults (paper background, ink text, the Tassyir type, 14px body) at zero
specificity, so any rule of yours that is not in a layer wins.
If your CSS uses cascade layers (Tailwind v4 does), name ours first, at the
very top of your stylesheet: layers declared first lose to the ones after,
and the package adds its sheet after yours.
Fonts
The dashboard sets Geist (Latin) and IBM Plex Sans Arabic. This package loads no font, so nothing leaves your page; the type falls back to the system’s sans. To show the exact faces, serve them yourself (both are under the SIL Open Font License) under those family names:Styling
Every element draws in its own shadow root, so your CSS cannot break it by accident, and the tokens reach inside. To adjust one, style the element itself (tsy-card { margin-block: 24px }) or a part:
tsy-text-field::part(box). Parts: base, control, label, hint,
error, box, prefix, suffix, row, heading, count, actions,
body, header, meta, back, toolbar, footer, scroll, dot,
media, dismiss, bar, link, options. To hide elements until they
are defined, add :not(:defined) { visibility: hidden }.
Accessibility
Built in: a label on every field, its hint and error read with it, a visible focus ring on every control, the arrow-key radio group, a 32px close button, live regions for banners and spinners, text pairs measured at 4.5:1 or more in both themes (a spec measures every pair the elements paint), and motion stopped or slowed underprefers-reduced-motion. Your page’s part:
the one <h1> is <tsy-page-header>’s; give icon-only buttons an
aria-label; mark loading regions aria-busy.
Content-Security-Policy, Trusted Types, server rendering
The elements write no<style> element and no style attribute (styles are
constructable stylesheets; colours set through the CSSOM) and never assign
HTML (createElement only). A page served with style-src 'self',
script-src 'self' and require-trusted-types-for 'script' works; the demo
is served that way. Importing the package on a server defines nothing and
throws nothing.
Size
Minified and gzipped, as an app’s bundler ships it (npm run size -w @tassyir/ui):
Most of it is CSS. The package is one entry today (
import '@tassyir/ui'
defines every element).
Checking against the dashboard
src/frontend-parity.test.ts compares this package with the dashboard’s own
design in the private tassyir/frontend repository: every colour token
against the palette blocks of app/[locale]/globals.css, string for string
in both themes; DESIGN.md’s front matter (colours, radii, spacing, type, the
button, input and card); and the classes of the dashboard’s primitives each
element mirrors. CI cannot reach that repository, so it runs locally:
origin/main (or TASSYIR_FRONTEND_REF) with git show; run
git -C ../frontend fetch origin first.
npm run browser-check -w @tassyir/ui (after npm run build) runs the demo
in a headless Chrome under a strict Content-Security-Policy: real form
submission and validity, the keyboard, the accessibility tree, and the demo
in Arabic and English, light and dark, at 390 and 1440 px, with a screenshot
of each (--out <dir>). npm run demo -w @tassyir/ui serves the demo at
http://localhost:4178/demo/.
Not here yet
React and JSX typings for the elements, acustom-elements.json manifest
for editors, per-element entry points, and more elements (tabs, a stat card,
a popover) come in later releases.