references/components.md
Component conventions
Read after SKILL.md. Two components carry most of the aesthetic: Button and Tile. Get those right and the rest follows.
Button - the CTA primitive
Apple has exactly two CTA forms, and picking the wrong one is the most common way an imitation reads as off.
type Variant = "link" | "pill";
type Tone = "default" | "on-dark";
link(the default) - coloured text followed by a›chevron. This is
most CTAs: Learn more, See specs, Compare. Apple uses it far more often than the pill.
pill- filled blue, 44px tall,radius: 980px. Reserved for high-intent
moments: Buy, Pre-order, Sign up. One per screen, usually.
Apple rarely mixes both in one CTA row. When it does, the link comes first.
tone="on-dark" swaps to the brighter blue (#2997ff) - the default #0066cc is unreadable on a black tile, and this is easy to miss because dark tiles are often built last.
Polymorphic by href
The component renders an <a> when given href, a <button> otherwise, and both look identical:
type AsAnchor = Common & { href: string } & Omit<AnchorHTMLAttributes<…>, keyof Common | "href">;
type AsButton = Common & { href?: undefined } & Omit<ButtonHTMLAttributes<…>, keyof Common>;
type ButtonProps = AsAnchor | AsButton;
This matters for accessibility: a CTA that navigates must be an anchor so it supports middle-click, open-in-new-tab, and screen-reader link semantics. A <div onClick> that looks like a button is the most common a11y failure on marketing sites.
The chevron is decorative and marked aria-hidden - it should not be announced.
Tile - the layout primitive
type Tone = "white" | "light" | "dark";
const TONES: Record<Tone, string> = {
white: "bg-surface-white text-ink-primary",
light: "bg-surface-light text-ink-primary",
dark: "bg-surface-black text-ink-on-dark",
};
Note the tone controls background and text together. That pairing is why you never hand-set a text colour on a dark tile, and why adding a fourth tone means auditing contrast rather than adding one line.
Defaults: min-h-[85vh] for heroes, py-[var(--spacing-tile-y)] (120px), centred column, overflow-hidden so break-out images cannot cause horizontal scroll.
size="compact" drops the min-height for secondary sections - a page of nothing but 85vh tiles feels like a slideshow.
Type
The scale is clamped and responsive, so headlines shrink on mobile without media queries:
--text-display: clamp(56px, 9vw, 96px);
--text-h1: clamp(48px, 7vw, 80px);
--text-h2: clamp(40px, 5vw, 56px);
--text-h3: clamp(28px, 3vw, 40px);
--text-body: 17px;
Tracking tightens as size increases - this is the giveaway:
--tracking-display: -0.022em;
--tracking-h1: -0.015em;
--tracking-h2: -0.010em;
--tracking-h3: -0.005em;
Body line-height is 1.47, matching apple.com. Display headlines override it.
font-feature-settings: "ss01", "cv11", "calt" is on globally - Apple runs contextual alternates and stylistic sets everywhere, and without them the type reads subtly generic even with the right family.
cn()
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]): string {
return twMerge(clsx(inputs));
}
clsx for conditionals, twMerge so a later class actually wins a Tailwind conflict. Without it, cn("px-6", "px-10") emits both and the winner depends on stylesheet order.
className always goes last in the cn() call so callers can override.
Motion
--ease-apple: cubic-bezier(0.28, 0.11, 0.32, 1);
--ease-out: cubic-bezier(0.16, 1, 0.3, 1);
Default ease-in-out looks generic; the Apple curve decelerates with a slight settle. A prefers-reduced-motion block flattens every duration to 0.01ms globally - keep it.
Inventory to build toward
atoms - Badge, Button, Display, Eyebrow, Input, Label, Toggle
molecules - CTARow, FormField, HeadlineBlock
organisms - Card, FeatureRow, Footer, HeroSection, Nav, PricingCard, PromoBanner, SpecSection, SplitTile, SubNav, Tile, plus commerce pieces (CheckoutStepper, OrderSummary, PaymentOption)
templates - MarketingTemplate, GalleryTemplate, PricingTemplate, ProductDetailTemplate, CheckoutTemplate
Note the shape: only three molecules, but fourteen organisms. That is the opposite of an admin system, and it is correct here - marketing pages are made of big distinct sections, not of small repeated controls.