Default theme
Moonstone and obsidian: the default materials, token roles and implementation values in Master UI.
On this page
Master UI's default theme has one material language. Light mode is moonstone, a clean zinc pearl. Dark mode is obsidian, a matte zinc glass whose surfaces step apart and keep a legible edge. Both are lit by one source above and slightly to the left, so every raised face catches a line of light on its top edge and settles on a short shadow that fades out.
The character is restrained: ink rather than color, half-pixel contours, corners that nest and depth that changes with state. Detail comes from light, not decoration, and no edge is harder than it needs to be to be found.
Use the sections below to choose a surface, line, text or depth role for your interface. The values describe the default theme in the current package; Theming shows how to override them or build another material.
Moonstone and obsidian
<div class="tray"> <div class="sheet flex flex-col gap-md p-lg"> <div class="flex items-center justify-between gap-md"> <span class="text-sm fg-text-strong">Production</span> <span class="badge badge-success badge-dot">Ready</span> </div> <input class="input" value="atlas.dev" aria-label="Domain"> <div class="flex items-center justify-between gap-md"> <label class="check"> <input type="checkbox" role="switch" class="switch" checked>Preview builds</label> <button type="button" class="btn btn-primary">Deploy</button> </div> </div></div>State and depth
State changes depth on the same control. Rest, hover and active use the raised, raised-hover and pressed shadow tokens respectively; active also takes an inset face. The default --lift moves the button up 0.5px on hover-capable pointers and down 0.5px while active. Cross-sections exaggerate depth. Use the live previews to try the actual interaction in both color schemes.
#Principles
Design explains the decisions behind the default materials, hierarchy, geometry and interaction states. Use that guidance to make design choices; this page documents the tokens and recipes implemented by @master/ui.
packages/ui/theme.css is the executable source of these values. Component styles read those tokens, and a theme such as Paper can replace them without rewriting the components.
#Surfaces
Surfaces form one ladder. Each step is a role, not a color: pick the step by what the surface does, and the theme resolves its value in light and dark.
The material ladder
<div class="tray"> <p class="m(0) px-md py-sm text-xs fg-text-muted">Tray</p> <div class="sheet flex flex-col gap-md p-lg"> <p class="m(0) text-xs fg-text-muted">Raised sheet</p> <div class="well p-md text-xs fg-text-muted">Inset well</div> <div class="sheet sheet-floating p-md text-xs fg-text-muted">Floating</div> </div></div>| Token | Role | Moonstone | Obsidian |
|---|---|---|---|
--color-surface-base | The page canvas | 98.6% | 19.4% |
--color-surface-tray | A recessed holder for sheets | 96% | 17.4% |
--color-surface-raised | A raised face: cards, buttons | 100% | 24.2% |
--color-surface-raised-low | The lower end of a raised face's gradient | 99.3% | 22.8% |
--color-surface-inset | A well: fields, tracks | 96.6% | 16.4% |
--color-surface-inset-hover | The same well under the pointer | 95.8% | 15.4% |
--color-surface-floating | Popovers and menus over content | 100% | 26.4% |
--color-surface-chip | Neutral chips and badges | 96.5% | white 7% |
--color-surface-inverse | Ink: primary buttons, checked controls | 22% | 95% |
--color-surface-scrim | The backdrop behind a modal | 18% at 20% opacity | black at 55% opacity |
Values are OKLCH lightness unless opacity is stated. A raised face is a top-to-bottom gradient from raised to raised-low, which @apply --surface-sheet writes for you. Choose a role by purpose: floating distinguishes a menu over a sheet, while inset marks a field within it. The color token does not position the element or give it a shadow.
#Lines
Lines are neutral ink alphas on moonstone and white alphas on obsidian. Two of them are 1px rules. The other three are contours, drawn as half-pixel rings with a zero-blur box-shadow, which takes no layout space; their alphas are tuned for that width, so a contour reads as fine rather than faint.
| Token | Moonstone | Obsidian | Drawn at | Use for |
|---|---|---|---|---|
--color-line-hair | 6.5% | 7.5% | 1px | Quiet separators inside a surface |
--color-line-divider | 12% | 13% | 1px | Rules between sections, tab rows |
--color-line-contour | 12% | 14% | .5px | The outline of every surface |
--color-line-strong | 20% | 23% | .5px | Hovered contours, switch tracks, keycaps |
--color-line-mark | 32% | 36% | .5px | Checkboxes, a focused field's contour |
A control that must be found at a glance takes a firmer contour than a surface does: a field stacks two contour rings, and a checkbox uses the mark.
#Text
Text is a soft ink, never pure black or white.
| Token | Use for |
|---|---|
--color-text-strong | Headings, labels and values |
--color-text-body | Running text |
--color-text-muted | Hints, captions and inactive options |
--color-text-disabled | Disabled controls |
--color-text-inverse | Text on ink |
--color-focus | Focus outlines |
#Elevation
Elevation is the order of depth, from a pressed well to an overlay. A raised face combines an inset highlight on its top edge, one half-pixel contour and three to six falling shades.
Elevation
<div class="p-lg r-lg bg-surface-inset shadow-pressed text-xs fg-text-muted">pressed</div><div class="p-lg r-lg bg-surface-raised shadow-flat text-xs fg-text-muted">flat</div><div class="p-lg r-lg bg-surface-raised shadow-raised text-xs fg-text-muted">raised</div><div class="p-lg r-lg bg-surface-raised shadow-raised-hover text-xs fg-text-muted">raised-hover</div><div class="p-lg r-lg bg-surface-raised shadow-lifted text-xs fg-text-muted">lifted</div><div class="p-lg r-lg bg-surface-floating shadow-floating text-xs fg-text-muted">floating</div>| Token | Use for |
|---|---|
--shadow-pressed | Wells and pressed controls |
--shadow-flat | A surface with one contour and no lift |
--shadow-raised | Buttons, chips and small cards at rest |
--shadow-raised-hover | The same under the pointer, with a firmer contour |
--shadow-lifted | Cards and panels |
--shadow-floating | Menus and popovers |
--shadow-overlay | Dialogs |
For example, --shadow-raised layers 0 1px 2px -1px, 0 2px 4px -2px and 0 4px 8px -4px beneath its highlight and contour. Higher elevations use a wider sequence of offsets. The negative spread keeps the shadow close to the object, while the blur softens its edge. These tokens draw depth; stacking order still comes from the element's layout and platform behavior.
#Ink
Ink is a face filled with the inverse color: a primary button, a checked checkbox, a sent message, the one inverse sheet of a page. Every ink face catches the same line of light on its top edge and takes the same hairline around it, so they read as one material at any size.
| Token | Role |
|---|---|
--color-surface-inverse-high, --color-surface-inverse-low | The fall of light on a sheet of ink |
--color-ink-high, --color-ink-low | The rounder fall on a control of ink, at rest |
--color-ink-high-hover, --color-ink-low-hover, --color-ink-pressed | The same control under the pointer and under a press |
--color-ink-highlight | The line of light on the top edge of any ink face |
--color-ink-edge | The hairline around it |
--color-surface-tip | A tooltip: ink on moonstone, a lighter slab on obsidian |
--shadow-ink, --shadow-ink-hover, --shadow-ink-pressed | The depth of a control of ink in each state |
--shadow-ink-mark | A small checked fill: a checkbox, a radio, a chosen day |
--shadow-ink-well, --shadow-thumb-on-ink | A filled track, and the thumb that rides it |
--shadow-tip | A tooltip and a chart's value |
A hued control (brand, a destructive confirmation) mixes its face, its edge and its glow from its own hue; --color-tone-highlight and --color-tone-highlight-soft are the light it catches.
#Thumbs, pictures and supplied colors
| Token | Role |
|---|---|
--color-thumb-high, --color-thumb-low | The pearl thumb of a switch, a slider and a comparison |
--color-thumb-on-high, --color-thumb-on-low | The thumb of a switch that is on |
--color-line-image, --color-line-image-strong | The inward line a picture and an avatar take, so neither bleeds into its surface |
--color-line-on-color, --color-highlight-on-color | The edge and the light on a color the page supplies, such as a swatch |
--color-on-image, --color-on-image-shade | What is drawn over a picture: white with a dark shade, in both themes |
--color-mark, --color-mark-underline | A highlighter stroke and its underline form |
--color-sheen | The light that passes over a skeleton |
--color-focus-halo | The soft halo a focused field wears outside its contour |
#Shape
Radii are roles, like surfaces. A chip is a small mark, a control is something pressed or typed in, and a surface is a sheet. Because the tokens are in @theme, they are utilities too: r-control uses the control's base radius. Components additionally apply the continuous-corner shape and scale described below; a radius utility alone does not reproduce that treatment.
| Token | Value | Used by |
|---|---|---|
--radius-chip | 6px | Badges, keycaps, checkboxes, skeletons |
--radius-control-sm | 8px | Small buttons and fields, menu rows, options, segmented thumbs |
--radius-control | 10px | Buttons, fields, wells |
--radius-control-lg | 12px | Large buttons and fields, alerts, choices, toasts |
--radius-surface | 16px | Sheets, cards |
A holder has no radius of its own. It derives its corner from what it holds: the inner radius plus the inset between them. A tray is a sheet's corner plus 4px, a menu is a row's corner plus 4px, a segmented track is its thumb's corner plus its inset, and a button inside a field is the field's corner minus 4px. The gap therefore keeps one width around the bend. Only pills, dots and avatars are rounder.
Nested corners
<div class="tray max-w(360) mx-auto"> <div class="sheet flex flex-col gap-md p-lg"> <div class="input-group"> <input class="input-group-field" value="atlas.dev" aria-label="Domain"> <button type="button" class="btn">Verify</button> </div> <div class="segmented" role="radiogroup" aria-label="Environment"> <label class="segmented-item"> <input type="radio" class="segmented-input" name="environment" checked>Production</label> <label class="segmented-item"> <input type="radio" class="segmented-input" name="environment">Preview</label> </div> </div></div>#Continuous corners
border-radius draws a quarter circle. Where a quarter circle meets a straight edge, the curvature jumps from zero to its full value at once, and the eye reads that joint as a shoulder. A squircle, drawn with corner-shape: squircle, starts turning earlier and tightens gradually, so the edge flows into the corner.
Components draw squircles where the browser supports corner-shape, and quarter circles elsewhere. A squircle hugs its corner more closely than a circle of the same radius, so a component multiplies its radius by --corner-scale to keep the same apparent roundness: 1.6 with squircles, 1 without. Both values are set once in the theme, and every component reads them.
| Property | With corner-shape | Without |
|---|---|---|
--corner-shape | squircle | round |
--corner-scale | 1.6 | 1 |
Pills, dots, avatars and thumbs stay true circles. Theming shows how to keep circular corners everywhere.
#Type
The theme does not download a font. The documentation uses Geist, and controls read the preset's font-family tokens. Load the font in your app before selecting it; the site's typography settings are:
@theme { --font-sans: 'Geist'; --font-feature-sans: 'ss02', 'ss03', 'ss04', 'ss06', 'ss07', 'ss08';}body { font-weight: 350;}Control labels use --font-weight-control (460), a light medium that reads clearly without looking heavy. Headings stay regular: size and ink carry the steps.
#Motion
One easing family keeps interaction quick and explanation calm, and one length sets how far a control travels: --lift is the half pixel a raised control rises under the pointer and sinks under a press. A card rises twice as far. Set it to 0px for a theme without travel; reduced motion already does.
| Token | Value | Use for |
|---|---|---|
--easing-glide | cubic-bezier(.22, 1, .36, 1) | Every state change |
--duration-press | 60ms | Pressing a control |
--duration-quick | 160ms | Hover and focus; popovers, menus and tooltips; any exit |
--duration-base | 280ms | Thumbs and panels that move; a dialog arriving |
--lift | .5px | How far a raised control rises and sinks |
Overlays write both directions in CSS. @starting-style gives the frame an element arrives from, and display and overlay transition with allow-discrete, so the exit plays before the element leaves the top layer. An exit is shorter than its entrance. With reduced motion, travel and scale are removed and the fade stays.
#Status and brand
--color-info, --color-success, --color-warning and --color-danger are the only hues the interface uses to speak. Each is tuned to read as text on its theme.
The brand color is the one hue that is not a status. It appears on btn-brand, once per view.
| Token | Value | Use for |
|---|---|---|
--color-brand | var(--color-indigo-50) | The face of a brand surface |
--color-brand-ink | near white | Text on a brand surface |
--color-brand-text | indigo-60 on moonstone, indigo-30 on obsidian | Brand-colored text on the page |
A brand surface mixes its edge, its light and its glow from --color-brand, so overriding that one token retunes it. Set --color-brand-ink to a dark ink if your brand color is light.
Status and brand
<span class="badge badge-info badge-dot">Info</span><span class="badge badge-success badge-dot">Success</span><span class="badge badge-warning badge-dot">Warning</span><span class="badge badge-danger badge-dot">Danger</span><button type="button" class="btn btn-brand btn-sm">Upgrade</button>#Color schemes
Colors that differ between schemes use light-dark(); some highlights and colors drawn over images stay the same in both. The theme follows the element's color-scheme. The Master CSS preset sets color-scheme: light dark on the root and lets you pin a scheme for the page or for one subtree:
<html data-theme="dark"> … <aside data-theme="light">A moonstone panel on an obsidian page</aside></html>There is no second stylesheet for dark mode and no dark-only class on components.
#Customize
See Theming to override tokens or build a complete theme, and Customization to choose between a utility, a token, a component property and owning the source.