>_devkit
apple-marketing-ui
skills/apple-marketing-ui/

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.