Foundations

Typography

Inter is the sole typeface across all Comitor surfaces. The package ships no font file — the app loads Inter through next/font/google with weights 400–800 and the Vietnamese subset, and publishes it as --font-inter.

How the font is wired

Two halves that have to match: the app declares the font and names the variable, the package reads that variable in its @theme block. Miss the app half and it fails quietly rather than loudly: var(--font-inter) carries no fallback, so the whole declaration is invalid at computed-value time and text inherits the browser's default face — the "Inter", system-ui, sans-serif tail written after it never gets a turn. Setting the variable is not optional.

app/layout.tsx
// app/layout.tsx — the APP loads Inter and publishes it as --font-inter.
// The package never bundles a font; it only consumes the variable.
import { Inter } from 'next/font/google'

const inter = Inter({
  subsets: ['latin', 'vietnamese'],
  weight: ['400', '500', '600', '700', '800'],
  display: 'swap',
  variable: '--font-inter',
})

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en" suppressHydrationWarning>
      <body className={`${inter.variable} font-sans antialiased`}>{children}</body>
    </html>
  )
}
css
/* @comitor/ui/styles.css — the two lines the package actually ships */
@theme inline {
  --font-sans: var(--font-inter), "Inter", system-ui, sans-serif;
  --font-mono: var(--font-inter-mono, "Geist Mono"), ui-monospace, monospace;
}
VariableWho sets itIf it is missing
--font-interThe app, as above. Required — the package assumes it.The var() has no fallback, so --font-sans is invalid at computed-value time and font-sans inherits the browser default. The list after it is never reached.
--font-inter-monoThe app, optionally. Changes the monospace face used by Kbd and chart tooltips.--font-mono falls back to "Geist Mono", ui-monospace, monospace.

The fallback for the mono face is written inside var(--font-inter-mono, "Geist Mono") rather than after it in the list, and that is deliberate: a var() with no fallback pointing at an undeclared variable makes the whole declaration invalid at computed-value time, so font-mono would inherit the surrounding font instead of continuing down the list. Almost no app sets --font-inter-mono, so it gets that in-var fallback; --font-inter is required of every app instead, which is why the sans line does not carry one.

Type scale — live samples

Design system · v1.0

text-xs font-bold tracking-[0.18em] uppercase

Comitor ships a single typeface — Inter — across every product surface. Generous line height ensures readability in dense admin UIs and marketing copy alike.

text-base leading-relaxed

The quick brown fox jumps over the lazy dog.

text-xl md:text-2xl font-semibold

The quick brown fox jumps over the lazy dog.

text-3xl md:text-4xl font-bold tracking-tight

The quick brown fox jumps over the lazy dog.

text-5xl md:text-6xl font-extrabold tracking-tight

Type scale table

The steps below are Tailwind's built-in scale. The package deliberately does not redeclare --text-* in its @theme block — there is no Comitor type scale to learn on top of Tailwind's. What the package owns instead is the root size, which is the next section.

NameTailwind classesSizeLine-heightUsage
Eyebrowtext-xs font-bold tracking-[0.18em] uppercase12px16px · 1.333Section labels, chip tags
Bodytext-base leading-relaxed16px1.625 (leading-relaxed)Prose, descriptions, labels
H3text-xl md:text-2xl font-semibold20–24px28–32px · 1.4 / 1.333Card titles, subsections
H2text-3xl md:text-4xl font-bold tracking-tight30–36px36–40px · 1.2 / 1.111Page section headings
Display H1text-5xl md:text-6xl font-extrabold tracking-tight48–60px1Hero headings, marketing

Sizes are the values at the default font-size step. Because the whole scale is expressed in rem, every number here moves when the user changes text size.

Text size is a user setting

One of the four display axes the package ships. It scales the root font size, not the type scale — so icons and control heights, which are sized off the spacing scale in rem, grow and shrink in step with the text rather than staying put while the labels change size around them.

StepSelectorDeclarationEffect
sm[data-font-size="sm"]font-size: 87.5%Root scales down; every rem value follows.
md(no attribute)The provider removes the attribute entirely. Nothing in the package touches the browser root size at this step.
lg[data-font-size="lg"]font-size: 112.5%Root scales up; icons and control heights scale with the text.
settings page
'use client'

// The font-size axis is a user setting, wired once in the root layout with
// <FontSizeProvider> and exposed anywhere with <FontSizeControl>.
// Default option labels are Vietnamese, so pass English ones explicitly.
import { FontSizeControl } from '@comitor/ui/shell'

export function DisplaySettings() {
  return (
    <FontSizeControl
      label="Text size"
      optionLabels={{ sm: 'Small', md: 'Medium', lg: 'Large' }}
    />
  )
}

Why percentages, and why the default step writes nothing

The two smaller and larger steps are expressed as a percentage of the inherited size, and the medium step emits no CSS at all. A reader may already have raised their browser's default text size; declaring an absolute font-size: 16px on <html> would erase that choice — breaking WCAG 1.4.4 from inside an accessibility feature. A percentage is a percentage of whatever they chose, so it is respected.

Rules

Do

  • Use Tailwind's scale directly — text-sm, text-base — and let the root size handle user preference.
  • Include the vietnamese subset. Without it, Vietnamese diacritics fall back to a system font mid-word.
  • Reach for font-mono for keyboard hints, code and tabular figures.

Do not

  • Do not introduce a third font family. Inter and the optional monospace face cover every need.
  • Do not set body text smaller than 14px in any product surface.
  • Do not set an absolute root font size — it cancels the text-size axis and the reader's own setting.
  • Do not redeclare --text-* in an app. The scale is Tailwind's and the package relies on that.