Typography
One typeface, one scale of semantic roles, and optical details that make text feel set rather than rendered.
Interfaces are mostly text. prfct treats typography as the primary material of the system: a single, highly legible family, a small set of named roles, and the quiet details — optical sizing, dynamic tracking, tabular figures — that separate a designed interface from a default one.
Faster builds, calmer dashboards
Incremental builds now reuse compiled modules across branches, cutting median deploy time by 38%. The dashboard was rebuilt on prfct, with denser tables and tabular figures that line up.
What changed
Heading, body and caption roles do the work: no one-off sizes, no stray weights.
import { Badge } from "@/components/ui/badge"
export default function TypeHierarchy() {
return (
<article className="flex w-full max-w-lg flex-col gap-3">
<h2 className="text-display-sm text-foreground">
Faster builds, calmer dashboards
</h2>
<div className="flex items-center gap-2 text-caption text-muted-foreground">
<Badge variant="secondary" size="sm">
v1.4
</Badge>
Published September 18 · 4 min read
</div>
<p className="text-body-md text-foreground/85">
Incremental builds now reuse compiled modules across branches, cutting
median deploy time by 38%. The dashboard was rebuilt on prfct, with
denser tables and tabular figures that line up.
</p>
<h3 className="mt-2 text-heading-sm text-foreground">What changed</h3>
<p className="text-body-sm text-muted-foreground">
Heading, body and caption roles do the work: no one-off sizes, no stray
weights.
</p>
</article>
)
}Typefaces
Inter
prfct is set in Inter 4.1, a variable family designed for screens. It carries two axes: weight, and optical size — the letterforms subtly redraw themselves with size. Text sizes get open counters and generous spacing; from 32px up, Inter's Display cut takes over with tighter spacing and finer details. You never switch fonts for headlines; the font adapts.
Aa
ABCDEFGHIJKLMNOPQRSTUVWXYZ abcdefghijklmnopqrstuvwxyz 0123456789 ąćęłńóśźż ÄÖÜß €$£ → ← ✓ ⌘ ⌥ ⇧
prfct self-hosts Inter rather than loading it from a font CDN. Hosted builds commonly strip OpenType features to save bytes — including the tabular figures data interfaces depend on. prfct's build keeps every feature and both axes, subsets to the Latin script (with full Polish, Czech, Turkish and other Latin Extended-A coverage), preloads only the upright style, and ships a metric-matched fallback so text doesn't reflow when the font arrives.
Geist Mono
Code, keyboard keys and machine-readable values use Geist Mono: compact, even-width, and designed to pair with neo-grotesque sans serifs.
Aa
ABCDEFGHIJKLMNOPQRSTUVWXYZ abcdefghijklmnopqrstuvwxyz 0123456789 ąćęłńóśźż ÄÖÜß €$£ → ← ✓ ⌘ ⌥ ⇧
Type scale
prfct names type by role, not by size. A role bundles size, line height, tracking and weight into one decision, so text styles can't drift apart one property at a time.
-0.024em
Deployments this week
Hero headlines on marketing surfaces.
-0.023em
Deployments this week
Page heroes.
-0.022em
Deployments this week
Section openers.
-0.021em
Deployments this week
Feature titles.
-0.02em
Deployments this week
Documentation page titles.
-0.021em
Quarterly revenue report
Top-level app page titles.
-0.019em
Quarterly revenue report
Section titles in product UI.
-0.016em
Quarterly revenue report
Card and dialog titles.
-0.011em
Quarterly revenue report
Group and panel titles.
-0.006em
Quarterly revenue report
Dense list and table headings.
-0.014em
Invoices go out on the first working day of the month and are due within 14 days. Paid ones move to the archive on their own.
Lead paragraphs, introductions.
-0.011em
Invoices go out on the first working day of the month and are due within 14 days. Paid ones move to the archive on their own.
Long-form reading.
-0.006em
Invoices go out on the first working day of the month and are due within 14 days. Paid ones move to the archive on their own.
Default product UI copy.
-0.003em
Invoices go out on the first working day of the month and are due within 14 days. Paid ones move to the archive on their own.
Secondary copy in dense UI.
-0.011em
Save changes
Large controls.
-0.006em
Save changes
Buttons, inputs, menu items.
-0.003em
Save changes
Compact controls, badges.
0em
Save changes
Micro labels, counters.
+0.002em
Updated 2 minutes ago
Captions, timestamps, helper text.
+0.08em
Foundations
Short uppercase labels: units, group labels in dense data. Never a label above a heading.
0em
const theme = resolveTheme({ brand })
Inline and block code (mono).
<h1 className="text-heading-xl">Settings</h1>
<h2 className="text-heading-md">Notifications</h2>
<p className="text-body-sm text-muted-foreground">Choose what we email you about.</p>Tailwind's own size utilities (text-sm, text-lg…) remain available for fine-grained UI work, and they carry prfct's tracking too.
Details that matter
Dynamic tracking
Letter spacing follows Inter's size-dependent metrics: slightly open at 12px, where letters need air, progressively tighter as size grows, where default spacing starts to look loose. Every role and every Tailwind size already includes the right value — don't add tracking-* to normal text.
A voice for headlines
Display roles switch on Inter's single-storey a (cv11) and rounded quotes (ss03). It gives large headlines a softer, more distinctive voice, while body and UI text keep the default double-storey a, which remains more legible at small sizes.
Figures that align
Numbers in tables, stats and timers must line up in columns. prfct tables turn on tabular figures by default; anywhere else, add the numeric utility.
| Frankfurt | 1,204,118 | 81.4 ms |
| Warsaw | 987,450 | 74.9 ms |
| Virginia | 11,009,871 | 112.0 ms |
| Frankfurt | 1,204,118 | 81.4 ms |
| Warsaw | 987,450 | 74.9 ms |
| Virginia | 11,009,871 | 112.0 ms |
const rows = [
{ region: "Frankfurt", requests: "1,204,118", p95: "81.4 ms" },
{ region: "Warsaw", requests: "987,450", p95: "74.9 ms" },
{ region: "Virginia", requests: "11,009,871", p95: "112.0 ms" },
]
function Column({ title, tabular }: { title: string; tabular: boolean }) {
return (
<div className="flex flex-col gap-2 rounded-xl border bg-card p-4">
<span className="text-xs font-medium text-muted-foreground">{title}</span>
<table
className="text-sm"
style={{
fontVariantNumeric: tabular ? "tabular-nums" : "proportional-nums",
}}
>
<tbody>
{rows.map((row) => (
<tr key={row.region} className="border-b last:border-0">
<td className="py-1.5 pr-6 text-foreground">{row.region}</td>
<td className="py-1.5 pr-6 text-right text-foreground">
{row.requests}
</td>
<td className="py-1.5 text-right text-muted-foreground">
{row.p95}
</td>
</tr>
))}
</tbody>
</table>
</div>
)
}
export default function TabularNumbers() {
return (
<div className="grid w-full max-w-2xl gap-3 sm:grid-cols-2">
<Column title="Proportional figures" tabular={false} />
<Column title="Tabular figures (numeric)" tabular />
</div>
)
}<span className="numeric">1,204,118</span>Balanced and pretty wrapping
Headings use text-wrap: balance so lines are even; paragraphs use text-wrap: pretty to avoid orphaned last words. Both are set globally.
Guidelines
- Use roles, not sizes. If a design needs a size that no role provides, it usually needs a different role.
- One style per heading level. Give each level one role, and skip levels in neither the markup nor the visual scale.
- Emphasize with weight, not size. A
font-mediumword insidebody-smis enough. - Keep reading width between 45 and 75 characters. Use
max-w-proseormax-w-2xlfor long text. - Sentence case everywhere — headings, buttons, menu items. Title Case reads as shouting in interfaces and makes names ambiguous.
Billing email
Invoices are sent to finance@acme.co.
Billing Email
Invoices Are Sent To finance@acme.co.
Accessibility
- Every size is set in
rem, so text scales with the reader's browser font size. Layouts reflow down to a 320px viewport, which is 400% zoom on a 1280px window. - Body roles use a line height of at least 1.5× for reading text and never set weights below 400 at small sizes.
- Muted text still meets 4.5:1; see Color.
- Uppercase is reserved for short overlines, which are tracked out and use case-sensitive punctuation so they remain readable.