Asyar Design Language — Measure
SkillMediaUse when building, modifying, or fixing any frontend UI in the Asyar launcher — new components, new views, layout changes, styling decisions, new built-in features, visual bug fixes, and anything in settings or onboarding. Answers "what do I use here?" for fonts, colour, typography, spacing, motion, components, and layout.
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the Asyar Design Language — Measure skill
What this skill tells your AI
The instructions your AI receives, as published by xoshbin/asyar in .agents/skills/design-language/SKILL.md and read by ahel’s review.
This file answers one question: given what you are building, what do you use?
Every table below is a lookup. Find your row, use what it says. If your case is not in a table, the answer is at the bottom under When nothing fits — it is never "invent something".
The reasoning behind all of this lives in
docs/reference/design-system/design-language.md.
Read that once. Work from this.
pnpm check:design enforces the mechanical half and runs in CI. If it
passes, you have not broken the system; it does not mean your UI is good. The
judgement calls are the tables.
The thesis, in one paragraph
Every value in Asyar is measured, not chosen.
Asyar is 750 × 480, of which 384px is content, and it is on screen for two to six seconds. Nothing in it has room to be arbitrary. A value is either derived from a constraint — a window dimension, a contrast ratio, a scale step, a count of rows that must fit — or it is a guess, and this is a system that does not ship guesses. Four principles, in priority order, and every table below is one of them made operational:
- Derived, Not Chosen — every value traces to a measurement or a scale step. "It looked right" is not a derivation.
- Subtract First — borders, fills, shadows and animations start at zero and have to be argued in. Remove it and look; if nothing is lost, it was decoration.
- One Candidate — exactly one element is the thing
⏎will act on, and nothing else may compete for the eye. - Every Affordance Names Its Key — if it can be done, its key is visible.
When two rules below seem to conflict, the principle wins, and the lower-numbered principle wins over the higher.
Restraint is the position here, not a fallback applied when nothing better comes to mind. If you are about to make something look more designed, that is the moment Principle II applies, not an exception to it.
The 30-second version
| You need | You use |
|---|---|
| Any UI element | A component from src/components/index.ts — read the barrel first |
| Any colour | A var(--…) token. Never a hex, rgb(), white, or a Tailwind colour |
| Any accent as text/icon | var(--accent-*) — the voice ramp |
| Any accent as a fill | var(--accent-*-fill) — the ground ramp. See §2 |
| Any text size or weight | A .text-* class, or var(--font-size-*) |
| Any letter-spacing | var(--tracking-*) — never a raw em |
| Any padding/margin/gap | var(--space-*) — the gap between things |
| Any width/height/icon size | var(--size-*) — the thing itself. See §5 |
| A shell metric | --shell-header-h · --shell-footer-h · --shell-row-h |
| Any corner | var(--radius-*) |
| Any duration | var(--dur-*) |
| Any easing | var(--ease-*) |
| Any stacking | var(--z-*) |
| A selected row | Let .list-row.selected / .selected-result do it — do not restyle it |
| A scrolling container | Add .custom-scrollbar |
| Something that does not exist | Build a reusable component in src/components/, export it from the barrel |
Never write a var(--token, fallback). Every token is always defined, so a
fallback can only ever hide a typo — that is exactly how a whole subsystem in
this app once ended up running on invented token names nobody noticed.
1. Fonts — which font, where
Two faces. There is no third.
| Where | Token | Renders as |
|---|---|---|
| All UI text — labels, titles, body, buttons, menus, settings, onboarding | var(--font-ui) | Satoshi |
| Code, JSON, file paths, IDs, hashes, shortcuts, timestamps, anything column-aligned | var(--font-mono) | JetBrains Mono |
--font-uiis applied globally to*. You only name it explicitly inside a component that also setsfont-familyfor another reason.- Satoshi renders on every platform, macOS included. There is deliberately no per-platform override. Asyar used to fall back to San Francisco on macOS; that gave the app no typographic identity on its primary platform. Do not reintroduce it.
- Reach for mono when characters need to line up or be read individually. A
duration, a count, or a version number in prose stays in
--font-ui; the same value in a table column goes mono..text-monoalready carriestabular-nums.
Tracking
| Token | Where |
|---|---|
--tracking-display | Page titles — the largest type in the app |
--tracking-tight | The command line, section headings |
--tracking-normal | All body, label and caption text |
--tracking-wide | Uppercase group labels only |
Never write a raw letter-spacing: -0.02em. The .text-* classes already
carry the right value, which is the main reason to use them.
2. Colour
The one rule people get wrong
A colour that fills cannot also be the colour that speaks.
There are two accent ramps, and picking the wrong one is a contrast bug that looks fine to you and is unreadable to someone else.
| Ramp | Token | Use for |
|---|---|---|
| The voice | --accent-* | Text, icons, strokes, focus rings, borders, and the source for a tint |
| The ground | --accent-*-fill | The background of a filled surface that carries --text-on-accent |
The test is mechanical: does this surface have --text-on-accent (or
--control-knob) on top of it? Yes → -fill. No → the voice.
/* A filled button, a checked checkbox, a user chat bubble, a coloured tile */
background: var(--accent-primary-fill);
color: var(--text-on-accent);
/* A status dot, a meter bar, a progress fill, a left accent seam — no text */
background: var(--accent-primary);
/* An accent-coloured label, a link, an icon */
color: var(--accent-primary);
check:design fails the build on a voice token used as a background in the
same rule as --text-on-accent (voice-as-fill). It cannot catch the inverse,
so: never use a -fill token as a text colour — it is too dark to read on
an Asyar surface.
Surfaces, from back to front
| Token | Use for |
|---|---|
--bg-primary | The window background. You rarely set this yourself |
--bg-secondary | Cards, sidebars, panels — anything sitting on the window |
--bg-tertiary | Inputs, wells, and subtle insets inside a card |
--bg-hover | Hover on any interactive row, button, or tile |
--bg-selected | The selected row — the only surface token carrying chroma |
--bg-popup | Opaque popups and menus |
--surface-canvas | Only where external HTML assumes a white page (URL previews) |
Never set an opaque background on the root container — platform overrides
(macOS transparency, Windows Acrylic, opaque Linux) handle that automatically.
Elevation is a scalar, not a stack: a popup opened from a card is still
one popup, not a card plus a popup. If you want a fourth level, what you
actually want is --scrim to push the thing beneath it back.
Text
| Token | Use for |
|---|---|
--text-primary | Headings, labels, the content the user came for |
--text-secondary | Subtitles, metadata, supporting detail |
--text-tertiary | Placeholders, hints, disabled text, timestamps |
--text-on-accent | Text or icons on any --accent-*-fill surface |
Three tiers. If you want a fourth, you want a different layout.
Colour is never the only channel
Every state must be legible with all hue removed. Simulated for deuteranopia (~6% of men) the three state colours collapse toward the same olive — success against danger sits at a luminance ratio of 1.23. No contrast tuning fixes that, because the problem is hue discrimination.
So a state colour is an accelerant on top of a shape, an icon, or a word, never
the sole carrier. A status indicator needs a label or an adjacent word; a
severity needs an icon with a different silhouette. Badge is fine — it carries
text. A bare coloured dot is not.
Review test: screenshot it, desaturate it, and see if you can still read the state.
Semantic state
| Token | Means |
|---|---|
--accent-primary | Primary action, focus, selection, links |
--accent-success | Succeeded, healthy, connected |
--accent-warning | Needs attention, degraded, in progress |
--accent-danger | Destructive action, error, failed |
--asyar-brand | Asyar's identity. The same blue as the accent — one identity colour, not two |
For a tinted background in a state colour, mix it rather than inventing a second token — this is the house formula, and it uses the voice ramp for both the tint and the text so they always agree:
background: color-mix(in srgb, var(--accent-danger) 12%, transparent);
color: var(--accent-danger);
Lines and edges
| Token | Use for |
|---|---|
--border-color | The border of an interactive element |
--separator | List dividers, section rules, subtle outlines |
--divider-soft | The faintest hairline (split-view handle) |
--rim-light | Edge highlight, inside a shadow or gradient — see §4 |
--rim-shade | Edge shade, same |
Prefer alignment to a border. Two things sharing a left edge already read as one column; a border on top of that is a line the layout has already drawn (Principle II). Use a real border when the job is "this is an interactive control with an outline".
Code colour
Only for rendered code and structured data. The Prism markdown theme and
JsonTree both read from these, so a snippet looks the same everywhere.
--syntax-comment · --syntax-keyword · --syntax-string · --syntax-number · --syntax-function
Everything else
--scrim / --scrim-opaque for modal backdrops · --control-knob for the
Toggle knob · --shadow-color inside shadows only · --kbd-rim for key chips.
3. Typography — which class, where
Use the class. Only drop to raw var(--font-size-*) inside a component that
needs a size the classes do not cover.
| Where you are | Class |
|---|---|
| The title of a settings page or a full-screen view | .text-page-title |
| A section heading inside a page | .text-section |
| An uppercase group label above a list | .section-header |
| The name of a list item, card, or result | .text-title |
| A paragraph of running text | .text-body |
| The label on a form field or setting | .text-label |
| The second line under a list-item title | .text-subtitle |
| A hint, a timestamp, a caption | .text-caption |
| Code, a path, an ID | .text-mono |
Weight rules: 500 (font-medium) is the heaviest weight for body and label
text. 600 (font-semibold) is for headings and the command line. 700 does
not appear in the product — at display sizes with display tracking, 600 is
already maximally emphatic and 700 reads as shouting. Never font-bold.
The command line is the largest text in the app and nothing may match it.
If you are adding something at --font-size-2xl or above outside
SearchHeader, you are competing with the query. Don't.
4. Elevation
Every drop shadow casts straight down. Every shadow is
0 Ypx.
Height comes from blur radius and negative spread, never from sliding the
shadow sideways. This is consistency, not physics: shadows that disagree about
their direction read as a rendering mistake, and the scale is easier to reason
about when height is the only variable. It binds drop shadows — an inset
box-shadow used to draw an edge is a drawing primitive, not a cast.
| Token | For |
|---|---|
--shadow-xs … -xl | Lift, in five steps |
--shadow-popup | Menus |
--shadow-launcher-popup | The launcher's floating surfaces |
--shadow-focus | Focus rings (applied globally already) |
--rim-light / --rim-shade are ingredients inside --shadow-launcher-popup
and the filled-button gradients. There are no .rim / .rim-all utility
classes — they existed, had zero call sites, and were removed.
Radius — --radius-xs (4) · sm (6) · md (8) · lg (10) · xl (12) ·
popup (20) · full. Small controls take sm, cards take md, modals take
xl, floating launcher surfaces take popup.
Stacking — never a bare number:
| Token | Value | For |
|---|---|---|
--z-base | 1 | Stacking inside one card or row |
--z-raised | 10 | An icon or handle over its own container |
--z-footer | 40 | The launcher bottom bars |
--z-dropdown | 50 | A list anchored to a field or row |
--z-floating | 60 | Toasts, search accessories — above dropdowns |
--z-header | 100 | The launcher search header |
--z-overlay | 200 | Prompts, banners, the extension inspector |
--z-portal | 9999 | A fixed popup escaping an overflow: hidden parent |
Modal uses native <dialog>, which renders in the browser's top layer and is
above all of these regardless of z-index. You never need to out-number a modal.
5. Space and size — which measurement, where
Space is the gap between objects. Size is the object. Two scales, and
reaching into the wrong one is how --space-* ended up with values like 11px
and 23px — those were an icon tile and a spinner, not rhythm.
| You are setting | You use |
|---|---|
| padding, margin, gap, inset | var(--space-*) |
| width, height, an icon, a tile, an avatar | var(--size-*) |
Space — a 2px grid
var(--space-N), from --space-0-5 (2px) to --space-11 (48px). Whole steps
only:
2 · 4 · 6 · 8 · 10 · 12 · 16 · 20 · 24 · 32 · 40 · 48
Four tokens are deprecated. Do not add new uses of them — they are the only off-grid values in the system, and they exist because sizes had no scale:
| Deprecated | Value | Use instead |
|---|---|---|
--space-1-5 | 5px | --space-1 or --space-2 |
--space-2-5 | 11px | --space-4 or --space-5 |
--space-5-5 | 13px | --space-5 or --space-6 |
--space-7-5 | 23px | --space-8, or --size-lg if it is an object |
Size — a 4px grid
| Token | Value | Canonical use |
|---|---|---|
--size-xs | 12px | A glyph inside a dense chip or badge |
--size-sm | 16px | The default inline icon, beside body text |
--size-md | 20px | A key chip, a small pill, a status pip |
--size-lg | 24px | The icon tile in a result row |
--size-xl | 32px | An extension avatar, a settings row icon |
--size-2xl | 48px | An empty-state or onboarding glyph |
--size-3xl | 64px | The largest object in the product |
The names are t-shirt, not role, so pick by value and use the canonical column
as a guide. --size-lg on a spinner is fine; it is 24px, not "a tile".
The nine-row fit — do not break this by accident
The launcher's content area is 384px, the list insets it by --space-3 top and
bottom, and 368px is exactly nine rows: 9 × 40 + 8 × 1.
- The row height is
var(--shell-row-h)(40px). Changing it changes how much of Asyar is visible at once. - Row separation is a raw
1px, not--space-0-5. Two pixels overflows 368 and costs a whole row. This is the one place a raw pixel beats a token. - Never hardcode
56px,40pxor384px. Use--shell-header-h,--shell-footer-h,--shell-row-h, orcalc()off them.
Density — how fast is this read?
The tokens are identical across all five surfaces; the rhythm is not.
| Surface | Rhythm | Read at |
|---|---|---|
| Launcher | --space-2 / --space-3 inside a row | A glance. Density is the feature |
| Settings | --space-5 / --space-6 between rows and sections | Deliberately, with a pointer |
| Onboarding | --space-7+ between blocks | Once, slowly. One idea per stage |
If you are unsure how much space to use, the question is not "what looks right" but "how fast is this read?"
Pointer targets: the launcher's 40px row is below the 44px guideline, which is an accepted trade for the ninth result on a keyboard-first surface. Nothing that is not keyboard-first gets that excuse — Settings and onboarding use the roomier density above.
6. Motion
Four durations, four curves. Every animation is one of each. There is no fifth of either — adding one is a change to the design language, not to your component.
| Duration | Use for |
|---|---|
--dur-instant | Colour/opacity landing under the pointer |
--dur-quick | A state change the user caused directly |
--dur-travel | A panel sliding |
--dur-emerge | A sheet opening |
| Curve | Use for |
|---|---|
--ease-travel | The default. Almost everything. |
--ease-emerge | Something arriving |
--ease-recede | Something leaving |
--ease-settle | Scale only. The only curve allowed to overshoot. |
Rules that are not negotiable:
- Arrival and departure use different curves. One curve for both is the fastest way to make an interface feel cheap.
--ease-settlemay only be applied toscale. Overshoot on position reads as sloppy; on opacity it is incoherent.- Before adding any animation, ask: if this were removed, would the user be confused about what happened? If no, don't add it.
- Reduced motion is handled globally in
style.css. You do not need aprefers-reduced-motionblock unless your component animates via JS.
Two animations are specifically not wanted. Both are the obvious thing to reach for, and both fail the test above:
- The selection travelling between rows. Selection snaps. The user pressed
↓; nobody is confused about where the highlight went. - The launcher animating on summon or dismiss. The window appears. An arrival animation delays the first usable moment on the one surface whose whole promise is being already there.
--transition-fast/normal/smooth/slow still exist and still work; they are
shorthands over the tokens above. New code should pair a duration with a curve
directly.
7. Voice — how it reads
Asyar is mostly words, and it writes the way an instrument reports: flatly, in the user's terms, without personality.
| Rule | Do | Not |
|---|---|---|
| Sentence case, always | "Copy to clipboard" | "Copy To Clipboard" |
| A control names its result | "Publish" → toast "Published" | "OK" · "Submit" |
| Name what the user recognises | "Notifications" | "Webhook config" |
| Errors say what broke and what next | "Extension failed to load — check the manifest" | "An error occurred" |
| No apology, no exclamation | "Nothing matched 'xyz'" | "Sorry! Nothing found!" |
| Second person, active | "Choose a shortcut" | "A shortcut should be chosen" |
- A result is named, never described. Title = what it is called. Subtitle = where it lives or what it does. Neither is a sentence.
- Truncate at the end, except file paths, where the middle goes.
- Fixed terms, not synonyms: a command is declared by an extension; an action is what you can do to a selected result; a result is a row; an extension ships them.
8. Components — which component, where
Shortened here. Read the whole file on GitHub.
Signals
- GitHub stars
- 782
- Forks
- 52
- Last commit
- Sep 2026
Advanced
- Catalog kind
- skill
- Gateway key
design-language- Source
- github.com/xoshbin/asyar