Build
Making it look designed
Personality first, one hero, space over lines, two greys, and the AI look to take out.
Everything else on this site makes a screen that works. This page is about the difference between a screen that works and one a designer would sign — and it is a short list, because most of that difference is restraint. Each rule is one decision and the habit that keeps it. The same rules go into the AGENTS.md that omaris init writes, so a model building in your project reads them before its first screen.
1. Decide the personality before the first screen
A journal is not a dashboard, and both on the default theme look like the same template. The product's character is one line in the provider, set before any page is written, and derived from what the app is rather than from what the defaults happen to be.
| The app is… | Provider theme | Frame |
|---|---|---|
| Personal — a journal, notes, reading | { preset: 'coffee', radius: 'md', font: 'serif', density: 'spacious' } | No sidebar. AppShellContent width="sm", one column, a TopAppBar |
| A dashboard, admin, ops | { preset: 'graphite', radius: 'sm', density: 'compact' } | AppShell + NavigationDrawer responsive, width="xl" |
| Consumer, phone-first | { preset: 'sunset', radius: 'md', font: 'rounded' } | NavigationBar in the footer, sheets not dialogs, width="md" |
| A developer tool | { preset: 'indigo', radius: 'none' } | Drawer collapsed, width="full", tables and code blocks |
| Money, health, anything that must feel safe | { preset: 'forest', radius: 'sm', density: 'comfortable' } | width="lg", fewer cards, more whitespace |
Corners stay at sm (the default) or md. lg, xl and full exist for a user who picks them in settings, not as a product default — every card, field and button balloons, and the screen reads as a toy. A softer personality comes from the font and the hue, not from rounder corners.
Or a hue of your own:
<OmarisProvider theme={{ preset: 'custom', hue: 25, chroma: 0.9, tint: 1.4 }}> A tint above 1 is the cheapest signature there is. The greys lean toward the accent, so even an empty surface belongs to this product and no other — which is what "our own look" means in practice. Pick once, write it at the top of the root layout, and never override it per page.
2. One hero per screen
Every screen has one thing the person came for. It gets the size — headline-lg, or display-sm for a number — the top-start position, and air around it. Everything else is a step down from it.
Scale
Five roles, three sizes each. The variant carries size, line height, letter spacing and weight together — if you find yourself adding font-semibold after it, the role is wrong, not the weight.
The quick brown fox
The quick brown fox
The quick brown fox
The quick brown fox
<script lang="ts"> import { Text } from 'omaris';</script><div class="flex w-full flex-col gap-4"> <div class="flex flex-col gap-1"> <Text variant="label-sm" tone="muted" font="mono">display-sm</Text> <Text variant="display-sm">The quick brown fox</Text> </div> <div class="flex flex-col gap-1"> <Text variant="label-sm" tone="muted" font="mono">headline-md</Text> <Text variant="headline-md">The quick brown fox</Text> </div> <div class="flex flex-col gap-1"> <Text variant="label-sm" tone="muted" font="mono">title-lg</Text> <Text variant="title-lg">The quick brown fox</Text> </div> <div class="flex flex-col gap-1"> <Text variant="label-sm" tone="muted" font="mono">body-md</Text> <Text variant="body-md">The quick brown fox</Text> </div> <div class="flex flex-col gap-1"> <Text variant="label-sm" tone="muted" font="mono">label-md</Text> <Text variant="label-md">The quick brown fox</Text> </div></div> A row of four equal stat cards has no hero. Make one number big and set the other three under it as title-md, or drop the row. The question to ask before laying anything out is "what is the one thing here?" — and if the answer is "the four cards", it is the wrong answer.
3. Space does the work
Whitespace before a line, a line before a shadow, and never a border and a shadow on the same box. Between sections gap-8 or gap-10; inside a card gap-3. Depth, where it is needed, is a surface tier — up one per level of nesting, and stop at three.
Surface tiers
The elevation ladder, as background tiers. Higher reads as closer.
surface-container-lowestsurface-container-lowsurface-containersurface-container-highsurface-container-highestsurface-variant<script lang="ts"> // Written out in full: Tailwind scans source text, so a class assembled at // runtime (`bg-{tier}`) would never be generated. const TIERS = [ ['bg-surface-container-lowest', 'surface-container-lowest'], ['bg-surface-container-low', 'surface-container-low'], ['bg-surface-container', 'surface-container'], ['bg-surface-container-high', 'surface-container-high'], ['bg-surface-container-highest', 'surface-container-highest'], ['bg-surface-variant', 'surface-variant'] ];</script><div class="grid w-full grid-cols-[repeat(auto-fill,minmax(11rem,1fr))] gap-3"> {#each TIERS as [cls, name] (name)} <div class="flex min-h-16 flex-col justify-end rounded-shape-md border border-border p-3 {cls}"> <code class="text-label-md text-foreground">{name}</code> </div> {/each}</div> Not everything is a card. A card is for a unit that stands alone — a stat, a product, a plan. A list of entries is rows, flex flex-col gap-1 with rounded-shape-md px-3 py-2 hover:bg-surface-container-high on each, not a card per row. A form is fields on the page, not a card around a card. And one level of nesting: a card inside a card inside a panel is three borders around one number.
Align everything to a single left edge and let the right rag. Centre only a marketing hero or an empty state.
4. Type is the design
Big and quiet. One large title, body-md for everything that is read, and above the title an eyebrow — label-md, uppercase, tracking-wide, tone="muted". That three-line stack, eyebrow → title → one muted sentence, is how nearly every good screen opens.
<div class="flex flex-col gap-1 pb-8"> <Text variant="label-md" tone="muted" class="tracking-wide uppercase">This week</Text> <Text as="h1" variant="headline-lg">Seven entries</Text> <Text tone="muted">Two more than last week. The longest was Tuesday.</Text></div> Two greys of text, not five: text-foreground and tone="muted". A third tone means the hierarchy has a hole in it. Numbers are always tabular, and a big number carries its unit small beside it. Prose has a measure — max-w-prose on a paragraph, width="sm" on a page that is mostly reading.
5. Colour is a role, and it is rare
The canvas is neutral. The accent goes on the primary action, the selected item, a link, and one detail per screen — a dot, an eyebrow, a ring. If bg-primary appears in more than two places, the screen is loud.
- A tinted area is a
-containerpair — `bg-primary-container
text-primary-container-foreground — never the role at /10`.
success,warning,destructiveandinfomean status and nothing else. A green card that is not "healthy" is a lie.- One filled button per screen; the rest step down the ladder.
- No gradients, no glow, no glass. They are what a template does when it has no idea.
bg-surface-container-lowbehind a section does the same job and reads as intentional.
6. The details that read as designed
Each of these is one line, and each is the difference between a mock-up and a product.
Icons and tones
check={false} keeps the icon when it is the meaning — a pin, a swatch; tone with tonal for a chip that reads as a state.
<script lang="ts"> import { Chip } from 'omaris';</script>{#snippet pin()} ...{/snippet}<Chip> {#snippet start()}{@render pin()}{/snippet} Pinned</Chip><Chip selectable selected check={false}> {#snippet start()}{@render pin()}{/snippet} Keeps its icon</Chip><Chip tone="success" variant="tonal">Paid</Chip><Chip tone="destructive" variant="tonal">Overdue</Chip><Chip size="sm">Small</Chip><Chip size="lg">Large</Chip> - An eyebrow above the title.
- The date, large, where a journal or a feed starts the day.
- A tonal
Chipfor a status, not a colouredBadgeon every row. - An
AvatarGroupwith its+3; aSparklineinside a stat; aProgressunder a quota. - Hover that tints (
hover:bg-surface-container-high), not hover that adds a border. An interactiveCardlifts on its own. - Icons from one set — Lucide matches the built-in glyphs — at
size-4beside text andsize-5in anIconButton. Never an emoji as an icon. - Rounded avatars, square-cornered thumbnails (
rounded-shape-md): one rule for pictures, kept everywhere. - The empty state exists on day one, because it is the first screen a new user sees.
Basic
An empty state should say what goes here and offer the way to make one.
No invoices yet
They will show up here the moment one is raised.
<script lang="ts"> import { Button, Empty } from 'omaris';</script><Empty class="w-full" bordered title="No invoices yet" description="They will show up here the moment one is raised."> {#snippet icon()} ... {/snippet} {#snippet actions()} <Button>New invoice</Button> {/snippet}</Empty> - Transitions on every state change —
duration-200 ease-standard, withmotion-reduce:transition-nonebeside them — and nothing that moves on its own.
7. Copy
Real names and real numbers; never "Lorem ipsum", never "John Doe", never "Sample item 1". Sentence case everywhere and no exclamation marks — no "Welcome to your dashboard!" and no "Manage your things with ease". Headings are nouns (Entries, This week); buttons are verbs (Write, Save, Invite); an empty state is one sentence and one verb ("Nothing written yet." → "Write the first entry"). A title is the word a person would say — "Journal", not "Journal Entry Management".
8. The AI look
If a screen has any of these, it was assembled rather than designed. Take it out:
- Four stat cards, then a chart, then a table — on every page, whatever the app.
- A centred hero with gradient text and three feature cards with an emoji each.
- A
Badgeon every row, a border on every box, a shadow on every card. - Every card with a header, a description, content and a footer.
rounded-2xlandshadow-lgsprinkled on things — both ignore the theme.- A toast on every button.
- Purple, on a product that has nothing to do with purple.
- Five shades of grey text and three sizes of heading in one card.
- A sidebar on an app with four pages.
Before you call it done
Open it at 390px and at 1440px, in light and in dark, and answer each of these with a yes:
- Can you name the one thing on the screen in three words?
- Is there exactly one filled button?
- Is there nothing with both a border and a shadow?
- Are there at most two greys of text?
- Does the empty state exist, and does it say what to do?
- Does every number line up, in Western digits, and every time read 12-hour with AM/PM?
- Does nothing move unless something happened?
- Would you show it as your own work?
A "no" on any of them is the next edit, not a note. Principles is why these rules hold; Building a screen is the order to build in.