Foundations
Typography
Material's five type roles, Geist, and Arabic that is actually set right.
Material's type scale, as five roles in three sizes each. The class carries the size, the line height, the letter spacing and the weight — so text-title-md is the whole decision. Adding font-semibold after it is a sign you picked the wrong role.
Scale
Each line is set with the class named on it. The class carries its own weight.
text-display-sm Display — the quick brown fox
text-headline-lg Headline — the quick brown fox
text-title-lg Title — the quick brown fox
text-body-lg Body — the quick brown fox
text-label-lg Label — the quick brown fox
<div class="flex w-full flex-col gap-4"> <div class="flex flex-col gap-1"> <code class="text-label-sm text-muted-foreground">text-display-sm</code> <p class="text-display-sm">Display — the quick brown fox</p> </div> <div class="flex flex-col gap-1"> <code class="text-label-sm text-muted-foreground">text-headline-lg</code> <p class="text-headline-lg">Headline — the quick brown fox</p> </div> <div class="flex flex-col gap-1"> <code class="text-label-sm text-muted-foreground">text-title-lg</code> <p class="text-title-lg">Title — the quick brown fox</p> </div> <div class="flex flex-col gap-1"> <code class="text-label-sm text-muted-foreground">text-body-lg</code> <p class="text-body-lg">Body — the quick brown fox</p> </div> <div class="flex flex-col gap-1"> <code class="text-label-sm text-muted-foreground">text-label-lg</code> <p class="text-label-lg">Label — the quick brown fox</p> </div></div> Use the component
Reach for Text rather than a <p> with a class on it. It names the same roles, picks the tag for you, and gives you the things you would otherwise hand-write every time — tone, clamping, tabular figures:
<Text as="h2">Deployments</Text><Text tone="muted">Every push to main builds and ships.</Text><Text variant="display-sm" tabular>48,210</Text> The utilities below still work, and are what Text is built on. Use them directly when you are styling something that is not a Text — a slot in a third-party component, or your own element.
Which role
| Role | For |
|---|---|
text-display-* | The one number on a landing screen. Rare in a dashboard. |
text-headline-* | Page and section titles. |
text-title-* | Card and dialog titles, table captions. |
text-body-* | Prose, cell contents, descriptions. |
text-label-* | Buttons, chips, tabs, column headers, form labels. |
Tailwind's own text-sm … text-5xl still work and are rewritten through the same knobs, so they gain the tracking. Reach for a role when the text has a job; reach for a size when it does not.
The knobs
Three custom properties tune the whole scale: --type-scale, --type-leading and --type-tracking. The theme store's fontSize setting moves the first, and because @theme inline keeps the tokens unresolved, every text utility on the page follows — including Tailwind's own.
Arabic
Arabic is not Latin at a different size. It has no capitals, so it reads smaller at the same nominal size; its ascenders and descenders need more room; and letter-spacing actively damages it because the letters join.
So :lang(ar) retunes the three knobs — slightly larger, more leading, zero tracking — and swaps in an Arabic-first stack. Nothing else changes: the same text-title-lg sets both scripts correctly.
Arabic
Arabic retunes the type knobs, so the same utilities set it correctly.
lang="en" Monthly revenue
Up 12% on last month.
lang="ar" الإيرادات الشهرية
بزيادة ١٢٪ عن الشهر الماضي.
<div class="flex w-full flex-col gap-6"> <div class="flex flex-col gap-1"> <code class="text-label-sm text-muted-foreground">lang="en"</code> <p class="text-title-lg" lang="en">Monthly revenue</p> <p class="text-body-md text-muted-foreground" lang="en">Up 12% on last month.</p> </div> <div class="flex flex-col gap-1" dir="rtl"> <code class="text-label-sm text-muted-foreground" dir="ltr">lang="ar"</code> <p class="text-title-lg" lang="ar">الإيرادات الشهرية</p> <p class="text-body-md text-muted-foreground" lang="ar">بزيادة ١٢٪ عن الشهر الماضي.</p> </div></div> It keys off language rather than direction, so an Arabic quote inside an English page is set properly, and an English string inside an RTL layout is not over-corrected.
Fonts
Geist and Geist Mono, imported separately so you can skip them:
'omaris/fonts.css'; The files ship with the package — no font CDN, and nothing to change for an app that runs offline. Each face is font-display: block, so text is drawn once, in the right face, rather than in a fallback that swaps a moment later. The theme's typefaces are declared there too and download only when chosen, and theme.font keeps the current face on screen until the new one has arrived — switching never flashes a fallback.
Preload the Latin files so the first paint doesn't wait on them either:
export const handle = ({ event, resolve }) => resolve(event, { preload: ({ type, path }) => type === 'js' || type === 'css' || (type === 'font' && /\/(geist|geist-mono)-latin\.[\w-]+\.woff2$/.test(path)) }); Or change the stack entirely — theme.font takes system, sans, rounded, serif, mono, or any font-family value of your own.
Numbers
Body text uses proportional figures, which read better in a sentence. Anything tabular should opt in with tabular-nums so columns of digits line up.