Asyar Design Language — Measure

SkillMedia

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

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:

  1. Derived, Not Chosen — every value traces to a measurement or a scale step. "It looked right" is not a derivation.
  2. 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.
  3. One Candidate — exactly one element is the thing will act on, and nothing else may compete for the eye.
  4. 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 needYou use
Any UI elementA component from src/components/index.ts — read the barrel first
Any colourA var(--…) token. Never a hex, rgb(), white, or a Tailwind colour
Any accent as text/iconvar(--accent-*) — the voice ramp
Any accent as a fillvar(--accent-*-fill) — the ground ramp. See §2
Any text size or weightA .text-* class, or var(--font-size-*)
Any letter-spacingvar(--tracking-*) — never a raw em
Any padding/margin/gapvar(--space-*) — the gap between things
Any width/height/icon sizevar(--size-*) — the thing itself. See §5
A shell metric--shell-header-h · --shell-footer-h · --shell-row-h
Any cornervar(--radius-*)
Any durationvar(--dur-*)
Any easingvar(--ease-*)
Any stackingvar(--z-*)
A selected rowLet .list-row.selected / .selected-result do it — do not restyle it
A scrolling containerAdd .custom-scrollbar
Something that does not existBuild 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.

WhereTokenRenders as
All UI text — labels, titles, body, buttons, menus, settings, onboardingvar(--font-ui)Satoshi
Code, JSON, file paths, IDs, hashes, shortcuts, timestamps, anything column-alignedvar(--font-mono)JetBrains Mono
  • --font-ui is applied globally to *. You only name it explicitly inside a component that also sets font-family for 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-mono already carries tabular-nums.

Tracking

TokenWhere
--tracking-displayPage titles — the largest type in the app
--tracking-tightThe command line, section headings
--tracking-normalAll body, label and caption text
--tracking-wideUppercase 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.

RampTokenUse for
The voice--accent-*Text, icons, strokes, focus rings, borders, and the source for a tint
The ground--accent-*-fillThe 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

TokenUse for
--bg-primaryThe window background. You rarely set this yourself
--bg-secondaryCards, sidebars, panels — anything sitting on the window
--bg-tertiaryInputs, wells, and subtle insets inside a card
--bg-hoverHover on any interactive row, button, or tile
--bg-selectedThe selected row — the only surface token carrying chroma
--bg-popupOpaque popups and menus
--surface-canvasOnly 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

TokenUse for
--text-primaryHeadings, labels, the content the user came for
--text-secondarySubtitles, metadata, supporting detail
--text-tertiaryPlaceholders, hints, disabled text, timestamps
--text-on-accentText 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

TokenMeans
--accent-primaryPrimary action, focus, selection, links
--accent-successSucceeded, healthy, connected
--accent-warningNeeds attention, degraded, in progress
--accent-dangerDestructive action, error, failed
--asyar-brandAsyar'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

TokenUse for
--border-colorThe border of an interactive element
--separatorList dividers, section rules, subtle outlines
--divider-softThe faintest hairline (split-view handle)
--rim-lightEdge highlight, inside a shadow or gradient — see §4
--rim-shadeEdge 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 areClass
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.

TokenFor
--shadow-xs-xlLift, in five steps
--shadow-popupMenus
--shadow-launcher-popupThe launcher's floating surfaces
--shadow-focusFocus 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:

TokenValueFor
--z-base1Stacking inside one card or row
--z-raised10An icon or handle over its own container
--z-footer40The launcher bottom bars
--z-dropdown50A list anchored to a field or row
--z-floating60Toasts, search accessories — above dropdowns
--z-header100The launcher search header
--z-overlay200Prompts, banners, the extension inspector
--z-portal9999A 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 settingYou use
padding, margin, gap, insetvar(--space-*)
width, height, an icon, a tile, an avatarvar(--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:

DeprecatedValueUse instead
--space-1-55px--space-1 or --space-2
--space-2-511px--space-4 or --space-5
--space-5-513px--space-5 or --space-6
--space-7-523px--space-8, or --size-lg if it is an object

Size — a 4px grid

TokenValueCanonical use
--size-xs12pxA glyph inside a dense chip or badge
--size-sm16pxThe default inline icon, beside body text
--size-md20pxA key chip, a small pill, a status pip
--size-lg24pxThe icon tile in a result row
--size-xl32pxAn extension avatar, a settings row icon
--size-2xl48pxAn empty-state or onboarding glyph
--size-3xl64pxThe 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, 40px or 384px. Use --shell-header-h, --shell-footer-h, --shell-row-h, or calc() off them.

Density — how fast is this read?

The tokens are identical across all five surfaces; the rhythm is not.

SurfaceRhythmRead at
Launcher--space-2 / --space-3 inside a rowA glance. Density is the feature
Settings--space-5 / --space-6 between rows and sectionsDeliberately, with a pointer
Onboarding--space-7+ between blocksOnce, 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.

DurationUse for
--dur-instantColour/opacity landing under the pointer
--dur-quickA state change the user caused directly
--dur-travelA panel sliding
--dur-emergeA sheet opening
CurveUse for
--ease-travelThe default. Almost everything.
--ease-emergeSomething arriving
--ease-recedeSomething leaving
--ease-settleScale 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-settle may only be applied to scale. 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 a prefers-reduced-motion block 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.

RuleDoNot
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