Design tokens

Fourteen tokens, mapped to Tailwind steps. Every text pairing meets WCAG AA.

signal

brand

Primary actions, links, active state

#6A4BCB

signal-hover

brand

Hover and pressed on filled buttons

#5A3DB5

signal-wash

violet-50

Tinted backgrounds, selected rows

#F5F3FF

ink

neutral-900

Titles and primary text

#171717

muted

neutral-500

Secondary text, meta, placeholders

#737373

paper

neutral-50

Page background

#FAFAFA

mist

neutral-200

Dividers, card and input borders

#E5E5E5

rating

yellow-500

Star icons only, never text

#F0B100

danger

rose-700

Errors, destructive actions

#C70036

danger-wash

rose-50

Cancelled badges, error tints

#FFF1F2

success

teal-800

Completed, confirmations

#005F5A

success-wash

teal-50

Completed badges

#F0FDFA

warn

orange-800

Pending, needs attention

#7E2A0C

warn-wash

orange-50

Pending badges

#FFF7ED

Tokens map to Tailwind steps where a close match exists. signal is brand-specific — the nearest step, violet-800, is far more saturated.

Headings

Six levels, all from UiHeading. Sans throughout — never serif. Pass as when the semantic tag differs from the visual level.

Inter everywhere, self-hosted — Latin, Latin Extended and Cyrillic, so en, ru and tr are covered. The native iOS app keeps SF Pro. One face per platform: no display cut, no heading-only family.

Notifications

page · text-[2rem] font-semibold · h1

Studio Nova

title · text-[1.6rem] font-semibold · h1

Balayage & toning

section · text-lg font-semibold · h2

Colour & highlights

group · text-base font-semibold · h3

Available times

ui · text-sm font-semibold · h3

Yesterday

label · text-sm · muted · h2

Full colour service with a licensed stylist, 2 hours

Body · text-sm · muted · not a heading

Heading hints

A muted line under a heading is part of that heading. Pass hint rather than writing a <p> after it — the mt-1 lives in the component.

Notifications

Choose how we reach you about bookings and reminders.

level="group" — a bold label above muted copy is a heading, so it lands in the outline as an <h3>:

Sign in to manage settings

Notification preferences are tied to your account.

The hint slot instead of the prop, when the copy carries markup:

Privacy

Read the privacy policy before deleting your account.

Not a hint — a value under a label is a pair of data lines and keeps its own tighter spacing:

In context

Studio Nova

4.9· 128 reviews

Full colour service with a licensed stylist, 2 hours

Radius

Two radii. Pills for things you toggle or dismiss, xl for everything else.

rounded-full

Chips, filters, tags, icon buttons, search

rounded-xl

Buttons, inputs, cards, sheets, modals

Buttons

White on signal 6.0:1 · signal on wash 5.5:1

Pill buttons — same styles, rounded-full:

Pills need a little more horizontal padding to look balanced.

Navigation chrome

Circular icon buttons, size-10, on a neutral fill. Back sits left, close sits right.

HeaderToolbar — back left, title centre, actions right:

Studio Nova

The title fades in on scroll past 24px. Transparent over content, white with a bottom border once scrolled.

FullDialog — centred title, close right:

Reschedule

Full-screen. Title is always visible, no scroll fade.

Sheet and Modal — drag handle, title left, no close on mobile:

Choose a time

Tuesday 12 August

Dismissed by dragging or tapping the backdrop, so no close button on mobile. Modal shows one on desktop only.

Icon button states:

Neutral by default. Signal fill only when the action is primary.

size-10 keeps the tap target at 40px. Every icon button needs an aria-label — there is no text to read.

Alerts

Inline notices. Same tints as status badges, so a colour means the same thing everywhere.

Your booking can be cancelled up to 24 hours before the appointment.
Booking confirmed. We sent the details to your phone.
This time overlaps another booking in your basket.

With a title, for anything longer than one line:

Two services overlap

Balayage runs until 15:30 and the manicure starts at 15:00. Move one of them to continue.

Dismissible:

Prices shown include VAT.

Errors get role="alert" so screen readers interrupt; the rest use role="status".

Loader

Three dots bouncing in sequence, brand colour, no fade. Inside a filled button they inherit the text colour instead.

sm
md
lg

In buttons:

On a signal fill the dots must inherit text colour — brand on brand is invisible.

Dates

Relative up to a month, then the day and month. The year appears only when it differs.

Renders a real <time> element and updates itself while on screen.

Prices

Thousands separated by a comma, no decimals. Never tabular-nums — numbers stay in the typeface's proportional figures.

00 TL
5050 TL
500500 TL
10001.000 TL
1250012.500 TL
12345671.234.567 TL

Use useMoney() — never format a price inline.

List rows

UiList owns the spacing; rows carry no margins of their own. UiListRow is the row — label, optional leading icon, optional trailing value, chevron by default. Renders a link when given to, otherwise a button.

plain — bare rows on a section:

divided — a rule between rows. Never hand-write the hr, and never suppress the last one with an index check:

as="div" — the row is not itself the control, so it must not be a button wrapping the switch:

Push notifications
SMS

variant="card" — standalone bordered rows:

variant="joined" — rows sharing one bordered container:

Empty states

No background of its own — edge-faded grid rules on the back, floating cards above the copy. pattern picks the cards: list (default) or grid. Always name the next action.

No favourites yet.

Tap the heart on any salon to save it here.

Without a hint:

No bookings here yet.

Compact — inside a card or section:

No free times on this day.

A search with no results is not an empty state — keep the query visible and offer a way back:

No places found

Nothing matches “balayage” in Ashgabat. Try a different service or area.

pattern="grid" — tiles, for galleries and card layouts:

No photos yet.

Work from this salon will show up here.

pattern="list" — rows, for feeds and lists (the default):

No reviews match this filter.

Skeletons

Bars use mist on white, pulsing on the whole row rather than each bar. Mirror the real layout so nothing jumps when content lands.

List row — thumbnail, title, two meta lines.

Detail header:

Chip row:

Text block — last line short, like real copy:

Skeletons are decorative — mark the container aria-hidden="true" and announce loading in one live region.

Loadable

UiLoadable owns the loading → empty → content chain, so a finished fetch with no rows never shows a skeleton. It marks the skeleton aria-hidden, puts animate-pulse on the container rather than each bar, and announces loading once in a live region.

Загрузка…

Gate on the request status, never on the row count alone — an empty array reads the same before the fetch starts and after it returns nothing.

Status badges

The six booking statuses. Tinted background, no border — badges label, they don't invite a tap.

ConfirmedPendingWaitingCompletedCancelledDraft

With a dot, when the badge sits in a dense row:

Confirmed Pending Waiting Completed Cancelled Draft

In a booking row:

Balayage & toning

Studio Nova · Tue 12 Aug, 14:00

Confirmed

Confirmed carries the brand colour, completed goes green. All sit on the 100 wash with 800 text, so no badge looks lighter than its neighbours. Lowest is confirmed at 5.1:1.

Counter badges

Counts use danger so they read as "needs attention" regardless of the accent.

3
12
99+

Cap at 99+. The bare dot is for "something is new" when the exact count doesn't help.

Inline counts, beside a label rather than over an icon:

Upcoming8
Unread messages3

Neutral for a plain total, danger only when it wants action.

Chips and filters

Selected state uses signal fill, not just a border.

Inline links

Bookings can be cancelled up to 24 hours before your appointment. See our cancellation policy for details, or read the terms of service.

ink link in muted body · 3.8:1 against surrounding text, underline matches the text

Standalone link, outside body copy:

View all reviews

Input

Error state uses danger on the border.

Pill suits search and single-field bars.

Borders use mist. Focus is the browser default.

Danger and rating

4.9· 128 reviews

This booking was cancelled.

The star is decorative, so it can be bright yellow — the number carries the rating in ink. danger 6.4:1 · white on danger 6.4:1