Skip to content
Brand guide

Interactive

Component gallery

These are not screenshots. Each example is the reference implementation in components/ui/ rendering live, so what you see is what you get when you copy the code. Written rules for each component are in the Components chapter.

Anything here is safe to lift into sincelabs/app as-is. Where the app currently differs, the app audit says so and why.

Buttons

Variants

Primary for the one action the page is for. Secondary for everything alongside it. Ghost for tertiary controls in dense chrome. Destructive only where something is actually destroyed.

<Button variant="primary">Save changes</Button>
<Button variant="secondary">Cancel</Button>
<Button variant="ghost">Dismiss</Button>
<Button variant="destructive">Delete organization</Button>

Sizes

lg (48px) on forms, auth and marketing. md (38px) inline in page chrome. sm (32px) inside table rows and toolbars only.

<Button size="lg">Large — forms</Button>
<Button size="md">Medium — inline</Button>
<Button size="sm">Small — table rows</Button>

Disabled and link-as-button

A link that looks like a button must still be an anchor — it navigates. Use buttonVariants() rather than wrapping a Button in a Link.

Back to sign in
<Button disabled>Signing in…</Button>

<Link href="/login" className={buttonVariants({ variant: "secondary" })}>
  Back to sign in
</Link>
Do

Name the action. The label completes “I want to…”.

Don't

“Submit”, “OK” and “Yes” tell the reader nothing about what is about to happen.

Status

Status chips

Six tones, each a background, a text colour and a dot. The dot carries the meaning when colour cannot — greyscale, colour blindness, a bad projector.

ActivePendingRejectedStale dataInternalRetired
<StatusChip status="active" />
<StatusChip status="pending" />
<StatusChip status="rejected" />
<StatusChip tone="warning" label="Stale data" />
<StatusChip status="internal" />
<StatusChip status="retired" />

Alerts

An inline message about the surface it sits on. error and warning carry role="alert"; the quieter tones do not, so a page of notes does not shout on load.

Password updated. You can sign in with it now.
Pinned apps lead the ⌘K switcher.
<Alert tone="error" title="That invite link has expired.">
  Ask your administrator to send a new one.
</Alert>

Forms

Text input

The label is always present and always tied to the input by id. A placeholder is not a label — it vanishes the moment someone types.

As it appears on your invoices.
Enter a valid email address.
<Input label="Work email" type="email" placeholder="[email protected]" />
<Input label="Company name" hint="As it appears on your invoices." />
<Input label="Work email" error="Enter a valid email address." />
Do

Visible label, placeholder shows the format. Survives typing, autofill and a screen reader.

Don't

Placeholder-as-label. Disappears on input, fails at 4.5:1, and is invisible to assistive tech.

Containers

Card and Panel

Card is a bare container. Panel adds the titled header rail — use it whenever the content needs a name, which is most of the time.

A bare card. Border first, shadow barely there — surfaces are separated by a hairline, not by floating.

Members

Everyone with access to this workspace.

Panel body.

<Card className="p-5">…</Card>

<Panel title="Members" description="Everyone with access." actions={<Button size="sm">Invite</Button>}>
  …
</Panel>

Page header

Every page opens with exactly this. Breadcrumb, optional eyebrow, serif title, description capped at 62ch, actions right.

Operator console

Review queue

Fee schedules the extractor was not confident about. Approving one publishes it to the register immediately.

<PageHeader
  breadcrumbs={[{ label: "Apps", href: "/apps" }, { label: "Sora" }]}
  eyebrow="Operator console"
  title="Review queue"
  description="Fee schedules the extractor was not confident about."
  actions={<Button size="md">Run ingestion</Button>}
/>

Data

Stat tiles

dt/dd pairs, so a screen reader reads the label with its value. Figures are tabular so a column of them lines up. At most one accent tile per grid.

Municipalities
309

Full national register

Current schedules
212

68% coverage

In review
14
Median fee
€1,840
<StatGrid columns={4}>
  <StatTile label="Municipalities" value="309" accent />
  <StatTile label="Current schedules" value="212" hint="68% coverage" />
</StatGrid>

Table

Wide tables scroll inside their own box rather than pushing the page sideways. The caption is visually hidden but tells a screen reader what the table is. Numbers right-aligned and tabular.

Recent access requests
CompanyStatusRequesterMembers
Maanrakennus Virtanen OyPending[email protected]12
Oteran Infra OyActive[email protected]147
<DataTable caption="Recent access requests" head={<><Th>Company</Th><Th numeric>Members</Th></>}>
  <Tr><Td>Maanrakennus Virtanen Oy</Td><Td numeric>12</Td></Tr>
</DataTable>

Empty state

Title says what is not here. Description says why or what to do. Action if there is one. Never a shrug.

No access requests yet

Requests submitted through the public form land here for review.

<EmptyState
  title="No access requests yet"
  description="Requests from the public form land here for review."
  action={<Button size="md">Invite someone</Button>}
/>

Identity

Eyebrow

The one place the brand uses uppercase. Always tracked out, always muted, always above a heading. The // device is optional and limited to one per surface.

//AI Native Lab

Operator console

<Eyebrow slash>AI Native Lab</Eyebrow>
<Eyebrow>Operator console</Eyebrow>

Avatar

Initials from a name, falling back to an email. Terracotta-ink fill so white initials clear AA; mist for a quieter, secondary context.

<Avatar name="Yuehan John" />
<Avatar name="Jaakko Mäkipelto" tone="mist" />
<Avatar email="[email protected]" size={44} />