Perplexity
MIT
Clean, accessible design system for a conversational AI search interface with light-first aesthetics, system fonts, and precise focus states
Layout StudioImport this kit into a Studio project and start editing.
CLI installRun it in any project. No account needed.
npx @layoutdesign/context install perplexityUI themeReskin shadcn or Layout UI components to match Perplexity.
npx shadcn add https://layout.design/r/perplexity/theme.json# layout.md — Perplexity.ai Design System
---
## 0. Quick Reference
> Copy-paste into `CLAUDE.md` or `.cursorrules` for immediate AI agent context.
**Stack:** Vanilla CSS + system-ui font stack. Token source: reconstructed from computed styles (low confidence — 0 native CSS custom properties found). All tokens are synthesised.
**How to apply:** Use as `var(--token-name)` in CSS, `style={{ prop: 'var(--token-name)' }}` in JSX, or `bg-[var(--token-name)]` in Tailwind.
```css
:root {
/* Colour */
--color-text-primary: rgb(49, 49, 49); /* All headings and body text */
--color-text-link: rgb(0, 0, 238); /* Default hyperlink colour */
--color-text-link-hover-dark: rgb(185, 214, 255); /* Link hover — dark theme */
--color-text-link-hover-light: rgb(0, 54, 129); /* Link hover — light theme */
--color-focus-ring: rgb(70, 147, 255); /* Focus/active outline colour */
--color-surface: rgba(0, 0, 0, 0); /* Transparent — no bg fills found */
--color-action-primary-hover-bg: rgb(0, 54, 129); /* CTA button hover background */
--color-action-primary-hover-text: rgb(255, 255, 255); /* CTA button hover text */
/* Typography — font stacks */
--font-sans: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
"Helvetica Neue", Arial, "Noto Sans", sans-serif,
"Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
--font-mono: monospace;
/* Spacing (8px grid) */
--space-xs: 8px;
--space-sm: 16px;
--space-md: 24px;
--space-lg: 32px;
--space-xl: 40px;
/* Focus ring */
--focus-outline: 2px solid rgb(70, 147, 255);
--focus-outline-offset: 2px;
--focus-radius: 2px;
/* Motion */
--ease-default: ease;
--duration-default: 200ms;
}
```
```tsx
// Production link component with all states
function ActionLink({ href, children }: { href: string; children: React.ReactNode }) {
return (
<a
href={href}
style={{
fontFamily: 'var(--font-sans)',
fontSize: '16px',
fontWeight: 400,
lineHeight: '24px',
color: 'var(--color-text-link)',
textDecoration: 'underline',
transition: `color var(--duration-default) var(--ease-default)`,
}}
className="action-link"
>
{children}
</a>
);
}
// CSS for states:
// .action-link:hover { color: var(--color-text-link-hover-dark); }
// .action-link:focus, .action-link:active {
// outline: var(--focus-outline); outline-offset: var(--focus-outline-offset);
// border-radius: var(--focus-radius); }
```
**NEVER rules:**
- NEVER use font-family other than `var(--font-sans)` or `var(--font-mono)`
- NEVER hardcode `rgb(49, 49, 49)` — always use `var(--color-text-primary)`
- NEVER omit `:focus` and `:active` outline states — use `var(--focus-outline)`
<!-- Quick Reference truncated to fit the 75-line cap. See later sections for the full design system. -->
## 1. Design Direction & Philosophy
### Character & Aesthetic Intent
Perplexity.ai's design language is **utilitarian and information-dense**. The interface is built to get out of the way of content — there is no decorative chrome, no warm colour palette, no gratuitous whitespace. The system reads as a neutral tool for knowledge retrieval: clean, direct, slightly cold.
The typeface is deliberately unpretentious: `system-ui` renders in whatever the OS considers its native sans-serif. This is a deliberate choice for performance and for reading-state comfort — the UI feels like the operating system, not a branded product.
### What This Design Explicitly Rejects
- **No decorative colour.** The palette is nearly monochrome (near-black text, transparent backgrounds). Accent colour appears only on interactive states (links, focus rings).
- **No rounded card surfaces.** Border radius is 0px on all structural elements; 2px only on focus outlines.
- **No warm tones.** The one accent is a cool blue (`rgb(70, 147, 255)`), with a darker navy for hover (`rgb(0, 54, 129)`). No red, orange, or amber.
- **No display typefaces.** No custom webfont loading overhead. The font stack is purely system-native.
- **No decorative shadows or elevation layers.** No `box-shadow` found on any extracted element.
- **No text transforms.** No `uppercase`, `capitalize`, or `small-caps`.
### Mood
Professional, fast, trustworthy, slightly austere. Comparable reference: a well-designed research paper viewer or a Reuters article page. The interface should feel like it loads instantly and never competes with its content.
---
## 2. Colour System
### Tier 1 — Primitive Values
```css
:root {
/* Greys */
--primitive-grey-950: rgb(49, 49, 49); /* Near-black: #313131 */
--primitive-grey-900: rgb(38, 38, 38); /* Darker near-black: #262626 */
--primitive-grey-000: rgb(255, 255, 255); /* White */
--primitive-transparent: rgba(0, 0, 0, 0);
/* Blues */
--primitive-blue-600: rgb(70, 147, 255); /* Focus/outline blue: #4693FF */
--primitive-blue-700: rgb(0, 54, 129); /* Hover blue (light mode): #003681 */
--primitive-blue-200: rgb(185, 214, 255); /* Hover blue (dark mode): #B9D6FF */
--primitive-blue-legacy: rgb(0, 0, 238); /* Browser-default link blue: #0000EE */
/* Reds (animation only — from @keyframes fade-in-animation) */
--primitive-red-800: rgb(178, 15, 3); /* Animation start fill: #B20F03 */
--primitive-grey-100: rgb(242, 242, 242); /* Animation end fill: #F2F2F2 — reconstructed */
}
```
### Tier 2 — Semantic Aliases
```css
:root {
/* Text */
--color-text-primary: var(--primitive-grey-950); /* All body, heading text — reconstructed */
--color-text-link: var(--primitive-blue-legacy); /* Default anchor colour — reconstructed */
--color-text-on-action: var(--primitive-grey-000); /* Text on filled action button hover — reconstructed */
/* Surfaces */
--color-surface-default: var(--primitive-transparent); /* Page/component bg — reconstructed */
/* Interactive states */
--color-focus-ring: var(--primitive-blue-600); /* Focus + active outline — reconstructed */
--color-link-hover-dark: var(--primitive-blue-200); /* Link hover on dark backgrounds — reconstructed */
--color-link-hover-light: var(--primitive-blue-700); /* Link hover on light backgrounds — reconstructed */
--color-action-hover-bg: var(--primitive-blue-700); /* Button/CTA hover background — reconstructed */
--color-action-hover-text-secondary: var(--primitive-grey-900); /* Subdued link hover — reconstructed */
}
```
### Tier 3 — Component Tokens
```css
:root {
/* Links */
--link-color-default: var(--color-text-link); /* reconstructed */
--link-color-hover-dark-context: var(--color-link-hover-dark); /* reconstructed */
--link-color-hover-light-context: var(--color-link-hover-light); /* reconstructed */
/* Focus ring (applies to links, inputs, interactive elements) */
--focus-outline-color: var(--color-focus-ring); /* reconstructed */
--focus-outline-width: 2px; /* reconstructed */
--focus-outline-offset: 2px; /* reconstructed */
--focus-border-radius: 2px; /* reconstructed */
/* Buttons (from .SWnb2:hover census) */
--button-hover-bg: var(--color-action-hover-bg); /* reconstructed */
--button-hover-color: var(--color-text-on-action); /* reconstructed */
--button-cursor-hover: pointer; /* reconstructed */
}
```
### Colour Mode Note
Two body class variants were detected — `.MDYf7` (dark mode context) and `.VMCj2` (light mode context) — based on the distinct hover colour directions:
| Mode | Class | Link hover | Focus ring |
|---|---|---|---|
| Dark | `.MDYf7` | `rgb(185, 214, 255)` — lighter blue | `rgb(70, 147, 255)` |
| Light | `.VMCj2` | `rgb(0, 54, 129)` — darker navy | `rgb(70, 147, 255)` |
**Focus ring colour is identical in both modes.**
---
## 3. Typography System
### Font Stacks
```css
:root {
--font-sans: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
"Helvetica Neue", Arial, "Noto Sans", sans-serif,
"Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
/* extracted: high confidence — matches all heading and body computed styles */
--font-mono: monospace;
/* extracted: high confidence — used on <code> elements */
}
```
### Composite Typography Groups
**NEVER declare font-size, font-weight, line-height, or letter-spacing in isolation. Always apply as a composite group.**
```css
/* ── Display / H1 ── */
/* Usage: Page title, primary heading */
.type-display {
font-family: var(--font-sans);
font-size: 40px; /* --perplexity-font-size-lg */
font-weight: 600; /* --perplexity-font-weight-medium */
line-height: 50px; /* --perplexity-line-height-loose */
letter-spacing: normal;
color: var(--color-text-primary);
/* extracted: high confidence */
}
/* ── Section Heading / H2 ── */
/* Usage: Section titles, card headings */
.type-heading {
font-family: var(--font-sans);
font-size: 24px; /* --perplexity-font-size-md */
font-weight: 600; /* --perplexity-font-weight-medium */
line-height: 30px; /* --perplexity-line-height-normal */
letter-spacing: normal;
color: var(--color-text-primary);
margin: 8px 0; /* reconstructed: moderate confidence — from h2 computed margin */
/* extracted: high confidence */
}
/* ── Body / Paragraph ── */
/* Usage: All prose, descriptions, paragraph content */
.type-body {
font-family: var(--font-sans);
font-size: 16px; /* --perplexity-font-size-sm */
font-weight: 400; /* --perplexity-font-weight-regular */
line-height: 24px; /* reconstructed: moderate confidence — body computed value, differs from --line-height-normal=30px */
letter-spacing: normal;
color: var(--color-text-primary);
margin-bottom: 32px; /* reconstructed: from body computed margin */
}
/* ── Caption / Small ── */
/* Usage: Labels, metadata, footnotes, navigation links */
.type-caption {
font-family: var(--font-sans);
font-size: 12px; /* --perplexity-font-size-xs */
font-weight: 400; /* --perplexity-font-weight-regular */
line-height: 18px; /* --perplexity-line-height-tight */
letter-spacing: normal;
color: var(--color-text-primary);
/* extracted: high confidence */
}
/* ── Code / Monospace ── */
/* Usage: Code snippets, technical strings, inline code */
.type-code {
font-family: var(--font-mono);
font-size: 12px; /* --perplexity-font-size-xs */
font-weight: 400;
line-height: 18px; /* --perplexity-line-height-tight */
letter-spacing: normal;
color: var(--color-text-primary);
display: inline;
/* extracted: high confidence */
}
```
### Weight Scale
| Token | Value | Usage |
|---|---|---|
| `--perplexity-font-weight-regular` | `400` | Body, captions, code, nav links |
| `--perplexity-font-weight-medium` | `600` | H1, H2 — all structural headings |
> Note: `font-weight: 600` is labelled "medium" in the token system but renders as semi-bold. No `700` (bold) or `300` (light) weights were detected.
### Pairing Rules
- **H1 → H2 → body** is the only confirmed hierarchy. Do not introduce additional heading levels without extracting their computed styles first.
- Code elements are always `12px / 18px / monospace` — do not scale code text to match surrounding body size.
- No italic styles were extracted. Do not introduce `font-style: italic` without design confirmation.
---
## 4. Spacing & Layout
### Base Unit & Scale
The spacing system operates on an **8px base grid**. All spacing values are multiples of 8px.
```css
:root {
--space-xs: 8px; /* --perplexity-space-xs — extracted: high confidence */
--space-sm: 16px; /* 2× base — reconstructed: high confidence, body font-size matches */
--space-md: 24px; /* 3× base — reconstructed: high confidence, h2 line-height matches */
--space-lg: 32px; /* 4× base — reconstructed: high confidence, body margin-bottom=32px */
--space-xl: 40px; /* 5× base — reconstructed: high confidence, h1 font-size=40px */
--space-2xl: 48px; /* 6× base — reconstructed: low confidence, inferred from scale */
}
```
### Breakpoints
```css
/* Extracted from media queries */
@custom-media --bp-mobile (max-width: 349px);
@custom-media --bp-sm (min-width: 350px); /* Mobile+ — extracted: high confidence */
@custom-media --bp-md (min-width: 720px); /* Tablet — extracted: high confidence */
@custom-media --bp-lg (min-width: 1024px); /* Desktop — extracted: high confidence */
```
| Breakpoint | Width | Context |
|---|---|---|
| mobile | < 350px | Smallest supported viewport |
| `--bp-sm` | ≥ 350px | Mobile baseline |
| `--bp-md` | ≥ 720px | Tablet / single-column-to-two-column transition |
| `--bp-lg` | ≥ 1024px | Desktop full layout |
### Container & Grid
```css
:root {
/* No explicit container widths were extracted. These are inferred from breakpoints. */
--container-max-width: 1024px; /* reconstructed: low confidence — matches largest breakpoint */
--container-padding-mobile: var(--space-sm); /* reconstructed: moderate confidence */
--container-padding-desktop: var(--space-lg); /* reconstructed: moderate confidence */
}
```
### Flex vs Grid Decision Rules
- **Flex (row):** Navigation bars, inline link groups, button rows, caption clusters. All extracted elements default to `flexDirection: row`.
- **Flex (column):** Single-column content stacks within a breakpoint container.
- **Grid:** Not detected in extracted computed styles. Do not introduce CSS Grid without confirming with extraction.
- **Gaps:** Use `--space-xs` (8px) for tight inline clusters; `--space-md` (24px) for section-level spacing; `--space-lg` (32px) for between major sections.
---
## 5. Page Structure & Layout Patterns
> **Note:** No screenshots were provided. This section is derived entirely from the layout digest, computed styles, and breakpoint data. Rows marked **(inferred)** have not been visually confirmed.
### 5.1 Section Map
| # | Section | Layout type | Approx. height | Key elements |
|---|---|---|---|---|
| 1 | Navigation / Header | Flex row | 48–64px | Logo, nav links (`.type-caption`), possibly a search input (inferred) |
| 2 | Hero / Search | Block, centred | 200–400px | H1, search input (`.Thnq5` + `.UOjrD8`), CTA button `.SWnb2` (inferred) |
| 3 | Feature / Content Body | Block or 2-col flex | 300–600px | H2, body paragraphs, inline links (inferred) |
| 4 | Footer / Legal | Flex row, wrap | 80–120px | Caption-size links (`.cpWvq6` context), privacy/legal anchors (inferred) |
### 5.2 Layout Patterns
**Header:**
- Flex row, `align-items: center`, `justify-content: space-between`
- Links rendered at `12px / 18px` (`.type-caption`)
- No background fill detected — sits against transparent or inherited surface
**Hero:**
- Centred block with H1 at `40px / 50px / 600`
- Contains a search input cluster (`.Thnq5` wrapping an `<input>` and a sibling `.UOjrD8` overlay for focus styling) — *inferred from interactive state extraction*
- CTA button `.SWnb2` sits below or beside input
- At `≥ 720px`: likely two-column or centred narrow column
- At `< 350px`: single column, full-width input
**Content / Feature area:**
- H2 headings with `margin: 8px 0` create tight vertical rhythm
- Body paragraphs have `margin-bottom: 32px` — this 32px gap is the primary between-paragraph spacer
- Links inline within body text
**Footer (`.cpWvq6` context):**
- Flex row, wrap
- Links hover to `rgb(38, 38, 38)` — subdued, not the blue accent
- Caption text size (`12px / 18px`)
### 5.3 Visual Hierarchy
- **H1** (`40px, 600`) is dominant; nothing competes with it at this size
- **H2** (`24px, 600`) creates a secondary tier — used for feature labels or section breaks
- **Body** (`16px, 400`) is the reading layer
- **Captions** (`12px, 400`) — navigation, metadata, legal — recede visually
- **CTA button** (`.SWnb2`) is the single action element: hover state fills with `rgb(0, 54, 129)` navy + white text — this is the most visually saturated element on the page
- **Focus ring** in `rgb(70, 147, 255)` bright blue is the only other high-contrast accent
### 5.4 Content Patterns
1. **Search input + CTA** is the primary interaction unit — likely centred on the page, full-width on mobile
2. **H2 + body paragraph** pairs repeat for feature or explanation sections — 8px gap above heading, 32px gap below paragraph
3. **Footer link row** uses caption-size text, subdued hover (dark grey rather than blue), suggesting secondary/legal navigation
4. **Interactive outline pattern** is consistent: `2px solid rgb(70, 147, 255)` with `border-radius: 2px` and `2px offset` — applied identically to links, inputs, and buttons
---
## 6. Component Patterns
### 6.1 Hyperlink (`<a>`)
**Anatomy:** Text node only. No icon or decoration beyond underline.
**State table:**
| State | `color` | `text-decoration` | Outline |
|---|---|---|---|
| Default | `rgb(0, 0, 238)` | `underline` | none |
| Hover (dark ctx) | `rgb(185, 214, 255)` | `underline` | none |
| Hover (light ctx) | `rgb(0, 54, 129)` | `underline` | none |
| Focus / Active | unchanged | unchanged | `2px solid rgb(70, 147, 255)`, offset 2px, radius 2px |
| Visited | [TBD — extract manually] | | |
| Disabled | [TBD — extract manually] | | |
```tsx
// Production hyperlink — all states handled
import React from 'react';
interface LinkProps {
href: string;
children: React.ReactNode;
context?: 'dark' | 'light';
className?: string;
}
export function Link({ href, children, context = 'light', className }: LinkProps) {
return (
<a
href={href}
className={`perplexity-link ${context === 'dark' ? 'perplexity-link--dark' : ''} ${className ?? ''}`}
>
{children}
</a>
);
}
/*
CSS to accompany:
.perplexity-link {
font-family: var(--font-sans);
font-size: 16px;
font-weight: 400;
line-height: 24px;
color: var(--link-color-default);
text-decoration: underline;
transition: color var(--duration-default) var(--ease-default);
}
.perplexity-link:hover {
color: var(--color-link-hover-light);
}
.perplexity-link--dark:hover {
color: var(--color-link-hover-dark);
}
.perplexity-link:focus,
.perplexity-link:active {
outline: var(--focus-outline-width) solid var(--focus-outline-color);
outline-offset: var(--focus-outline-offset);
border-radius: var(--focus-border-radius);
}
*/
```
---
### 6.2 Search Input (`.Thnq5` / `.UOjrD8`)
**Anatomy:** A hidden `<input>` element with a styled sibling `.UOjrD8` overlay that receives focus styling. The visual input container is the overlay, not the native input.
**State table:**
| State | Visual outline | Background |
|---|---|---|
| Default | none | [TBD — extract manually] |
| Focus | `2px solid rgb(70, 147, 255)`, offset 2px, radius 2px | inherited |
| Active | `2px solid rgb(70, 147, 255)`, offset 2px, radius 2px | inherited |
```tsx
// Search input with accessible focus state
export function SearchInput({ placeholder = 'Ask anything...' }: { placeholder?: string }) {
return (
<div className="search-wrapper">
<input
type="text"
className="search-input-hidden"
placeholder={placeholder}
aria-label={placeholder}
/>
<div className="search-overlay" aria-hidden="true" />
</div>
);
}
/*
CSS:
.search-wrapper {
position: relative;
font-family: var(--font-sans);
font-size: 16px;
font-weight: 400;
line-height: 24px;
}
.search-input-hidden:focus ~ .search-overlay,
.search-input-hidden:active ~ .search-overlay {
outline: 2px solid var(--focus-outline-color);
outline-offset: var(--focus-outline-offset);
border-radius: var(--focus-border-radius);
}
*/
```
---
### 6.3 CTA Button (`.SWnb2`)
**Anatomy:** Single text label. No icon detected. Cursor changes to `pointer` on hover.
**State table:**
| State | Background | Text colour | Border | Cursor |
|---|---|---|---|---|
| Default | [TBD — extract manually] | [TBD] | [TBD] | default |
| Hover | `rgb(0, 54, 129)` | `rgb(255, 255, 255)` | `rgb(0, 54, 129)` all sides | `pointer` |
| Focus | [TBD] | [TBD] | focus ring: `2px solid rgb(70, 147, 255)` | pointer |
| Active | same as hover | same as hover | focus ring | pointer |
| Disabled | [TBD — extract manually] | | | `not-allowed` |
```tsx
// CTA Button — full state handling
interface ButtonProps {
children: React.ReactNode;
onClick?: () => void;
disabled?: boolean;
type?: 'button' | 'submit';
}
export function CTAButton({ children, onClick, disabled = false, type = 'button' }: ButtonProps) {
return (
<button
type={type}
onClick={onClick}
disabled={disabled}
className={`cta-button ${disabled ? 'cta-button--disabled' : ''}`}
>
{children}
</button>
);
}
/*
CSS:
.cta-button {
font-family: var(--font-sans);
font-size: 16px;
font-weight: 600;
line-height: 24px;
letter-spacing: normal;
cursor: default;
transition: background-color var(--duration-default) var(--ease-default),
color var(--duration-default) var(--ease-default),
border-color var(--duration-default) var(--ease-default);
}
.cta-button:hover {
background-color: var(--button-hover-bg); /* rgb(0, 54, 129) */
color: var(--button-hover-color); /* rgb(255, 255, 255) */
border-color: var(--button-hover-bg);
cursor: pointer;
}
.cta-button:focus,
.cta-button:active {
outline: 2px solid var(--focus-outline-color);
outline-offset: var(--focus-outline-offset);
border-radius: var(--focus-border-radius);
}
.cta-button--disabled {
opacity: 0.4;
cursor: not-allowed;
pointer-events: none;
}
*/
```
---
### 6.4 Navigation Link (`.cpWvq6` context)
**Anatomy:** Anchor tag inside a navigation container. Footer/secondary navigation pattern.
**State table:**
| State | Colour |
|---|---|
| Default | `rgb(0, 0, 238)` or `var(--color-text-primary)` (context-dependent) |
| Hover | `rgb(38, 38, 38)` — dark grey, NOT the blue accent |
| Focus / Active | `2px solid rgb(70, 147, 255)` outline, radius 2px, offset 2px |
```tsx
export function NavLink({ href, children }: { href: string; children: React.ReactNode }) {
return (
<a href={href} className="nav-link">
{children}
</a>
);
}
/*
CSS:
.nav-link {
font-family: var(--font-sans);
font-size: 12px;
font-weight: 400;
line-height: 18px;
text-decoration: none;
color: var(--color-text-primary);
transition: color var(--duration-default) var(--ease-default);
}
.nav-link:hover {
color: rgb(38, 38, 38); /* --primitive-grey-900 */
}
.nav-link:focus,
.nav-link:active {
outline: 2px solid var(--focus-outline-color);
outline-offset: var(--focus-outline-offset);
border-radius: var(--focus-border-radius);
}
*/
```
---
### 6.5 Code Block / Inline Code
**Anatomy:** `<code>` inline element. No background or border-radius extracted.
**State table:** Not interactive — no hover/focus states.
```tsx
export function InlineCode({ children }: { children: React.ReactNode }) {
return <code className="inline-code">{children}</code>;
}
/*
CSS:
.inline-code {
font-family: var(--font-mono);
font-size: 12px;
font-weight: 400;
line-height: 18px;
letter-spacing: normal;
color: var(--color-text-primary);
display: inline;
background-color: [TBD — extract manually];
border-radius: [TBD — extract manually];
padding: [TBD — extract manually];
}
*/
```
---
## 7. Elevation & Depth
### Findings
**No box-shadow, filter, or backdrop-filter values were detected on any extracted element.** All computed `box-shadow` values returned `none`. This is consistent with the flat, austere design direction.
```css
:root {
/* No shadow scale exists in the current design system */
--shadow-none: none; /* The only elevation token. All elements are flat. */
/* Z-index scale — reconstructed: low confidence, no z-index was extracted */
--z-base: 0;
--z-raised: 10; /* reconstructed: dropdowns, tooltips */
--z-overlay: 100; /* reconstructed: modals, overlays */
--z-toast: 1000; /* reconstructed: notifications */
}
```
### Border Tokens
```css
:root {
/* No borders were detected on structural elements */
/* All extracted border values were "0px none rgb(49, 49, 49)" */
--border-none: none; /* reconstructed */
--border-width-focus: 2px; /* The ONLY border-width in the system — for focus rings only */
--border-color-focus: rgb(70, 147, 255); /* var(--color-focus-ring) */
}
```
> **NEVER** add decorative borders or shadows to components. The design system has zero elevation — the only bordered element is the focus outline, which is functional, not decorative.
---
## 8. Motion
### Timing Tokens
```css
:root {
--ease-default: ease; /* extracted: high confidence — 8 elements */
--duration-default: 200ms; /* reconstructed: moderate confidence — not explicitly extracted, inferred as standard */
--transition-all: all var(--duration-default) var(--ease-default);
/* Note: computed styles show transition: "all" — Perplexity applies transition to all properties.
Be explicit in production to avoid unintended transitions on layout/transform properties. */
}
```
### Keyframe Animations
These are extracted from the stylesheet but are system/utility animations, not brand motion:
| Token | Keyframe | Usage |
|---|---|---|
| `--motion-spin` | `rotate 0→360deg` | Loading spinners |
| `--motion-scale` | `scale none → scale3d(1,1,1) → none` | Pulse/breathe effects |
| `--motion-stroke` | `stroke-dashoffset → 0` | SVG path draw-on |
| `--motion-scale-up-center` | `scale(0.01) → scale(1)` | Modal/element entrance |
| `--motion-fade-in` | `opacity 0 → 1` | Content fade-in |
| `--motion-fireworks` | `scale+opacity: 0→1.5→2, 0→1→0` | Celebration effect |
| `--motion-firework` | `stroke-dashoffset draw + opacity` | SVG celebration |
| `--motion-unspin` | `stroke-width + dashoffset` | Spinner completion |
| `--motion-fade-in-animation` | `fill/stroke red → grey` | Colour state animation |
| `--motion-dots` | `content "" → "." → ".." → "..."` | Loading ellipsis (pseudo-element) |
| `--motion-lds-ring` | `rotate 0→360deg` | Ring spinner |
### Motion Rules
- Use `var(--ease-default)` (CSS `ease`) for all transitions
- **Do not** animate `all` properties in production — the site does this but it causes layout thrashing. Be explicit: `transition: color 200ms ease, background-color 200ms ease`
- `--motion-fade-in` and `--motion-scale-up-center` are the correct entrance patterns
- Do not introduce `cubic-bezier` custom easing — no custom easing curves were detected
- Reduce motion: wrap animations in `@media (prefers-reduced-motion: no-preference)` — not extracted, but required for accessibility
---
## 9. Anti-Patterns & Constraints
**1. Hardcoding colour hex values instead of tokens**
**Rule:** Never write `color: #313131` or `color: rgb(49, 49, 49)` directly in component code.
**Why it fails:** AI agents default to hardcoding values from designs. When the dark/light mode class changes (`body.MDYf7` vs `body.VMCj2`), hardcoded values don't adapt, causing colours that are correct in one mode to be wrong in the other.
**Instead:** Always use `var(--color-text-primary)`, `var(--color-link-hover-dark)`, etc.
---
**2. Using arbitrary spacing values off the 8px grid**
**Rule:** Never use spacing values like `10px`, `14px`, `20px`, `28px`.
**Why it fails:** AI agents often interpolate "visually appropriate" spacing from surrounding values. Perplexity uses a strict 8px grid (8, 16, 24, 32, 40, 48). Non-grid values create visual rhythm breaks that compound across sections.
**Instead:** Use only `var(--space-xs)` through `var(--space-2xl)` — or 8px multiples explicitly.
---
**3. Defaulting to Inter, Roboto, or Arial as the primary font**
**Rule:** NEVER set `font-family: 'Inter', sans-serif` or any named webfont.
**Why it fails:** AI agents are trained heavily on designs using Inter. Perplexity uses `system-ui` as the primary, meaning the font renders as San Francisco on macOS, Segoe UI on Windows, and the OS default on Android. Adding Inter changes the rendering character across all platforms and introduces a network font load that the design explicitly avoids.
**Instead:** Always use `var(--font-sans)` which resolves to the full system-ui stack.
---
**4. Omitting focus and active states**
**Rule:** NEVER ship an interactive element without explicit `:focus` and `:active` styles.
**Why it fails:** AI agents generating component code frequently implement only `:hover`. Perplexity's extracted CSS shows the focus pattern is consistently `2px solid rgb(70, 147, 255)` with `outline-offset: 2px` and `border-radius: 2px`. Missing this fails WCAG 2.4.7 and breaks keyboard navigation.
**Instead:** Every interactive element must have the focus block: `outline: 2px solid var(--focus-outline-color); outline-offset: 2px; border-radius: 2px;`
---
**5. Adding decorative shadows or border-radius to cards and containers**
**Rule:** NEVER add `box-shadow`, `border-radius > 2px`, or decorative `border` to structural containers.
**Why it fails:** AI agents default to "modern card UI" conventions with `border-radius: 8px`, `box-shadow: 0 2px 8px rgba(0,0,0,0.1)`. Zero such values exist in Perplexity's extracted styles. Adding them contradicts the flat, austere design intent and makes the UI look like a generic SaaS product.
**Instead:** Keep all containers `border-radius: 0px`, `box-shadow: none`. The only `2px` radius is on focus outlines.
---
**6. Constructing Tailwind classes dynamically**
**Rule:** NEVER write `className={\`text-${size}\`}` or template-literal Tailwind classes.
**Why it fails:** Tailwind's PurgeCSS step removes classes it cannot statically analyse. Dynamic class construction causes styles to silently disappear in production builds.
**Instead:** Use static class maps: `const sizeClass = size === 'lg' ? 'text-[40px]' : 'text-[16px]'` or use CSS custom properties via `style={{ fontSize: 'var(--perplexity-font-size-lg)' }}`.
---
**7. Using `transition: all` in production components**
**Rule:** NEVER use `transition: all` in component stylesheets.
**Why it fails:** Perplexity's computed styles show `transition: all` — this is the browser default carry-through, not a deliberate declaration. Copying it into components causes transitions on `width`, `height`, `display`, and `transform`, triggering layout recalculations and janky resize behaviour.
**Instead:** Be explicit: `transition: color 200ms ease, background-color 200ms ease, border-color 200ms ease;`
---
**8. Using `!important` to override theme colours**
**Rule:** NEVER use `!important` for colour, spacing, or typography overrides.
**Why it fails:** The dark/light mode system uses body class specificity (`.MDYf7 a:hover`, `body.VMCj2 a:hover`). Adding `!important` to component styles will win against the theme class selectors, permanently overriding the mode-aware colour system for that element.
**Instead:** Match or beat the specificity of the theme selector: `.MDYf7 .my-component:hover { color: var(--color-link-hover-dark); }` — no `!important` required.
---
**9. Using absolute positioning for layout flow**
**Rule:** NEVER use `position: absolute` to place elements that belong in document flow.
**Why it fails:** AI agents sometimes use absolute positioning for alignment problems (e.g., vertically centring text in a button). This removes elements from the flex flow, breaking at the 350px, 720px, and 1024px breakpoints where the layout restructures.
**Instead:** Use flex layout with `align-items: center` and `justify-content: center` on the parent.
---
**10. Leaving link hover states as the wrong colour for the context**
**Rule:** NEVER apply `--color-link-hover-dark` in a light-mode context or vice versa.
**Why it fails:** `rgb(185, 214, 255)` (the dark-mode hover) is near-invisible on a light background. AI agents grabbing the first hover value from the extraction will use it universally. The system has two hover colours keyed to body class.
**Instead:** Use the body class selectors: `body.VMCj2 .my-link:hover { color: var(--color-link-hover-light); }` and `body.MDYf7 .my-link:hover { color: var(--color-link-hover-dark); }`.
---
## Appendix A: Complete Token Reference
Every token extracted from the source. §0 CORE TOKENS is the primary AI signal; this appendix is reference material an AI can cross-check against when a curated role is missing.
```css
/* Colours (58) */
--color-text-primary: rgb(49, 49, 49);
--color-surface-page: #ffffff;
--color-link-default: #0000ee;
--color-link-hover-light: rgb(0, 54, 129);
--color-link-hover-dark: rgb(185, 214, 255);
--color-focus-ring: rgb(70, 147, 255);
--color-cta-bg-hover: #003681;
--primitive-black: #000000;
--primitive-white: #ffffff;
--primitive-gray-900: #313131;
--primitive-gray-200: #f2f2f2;
--primitive-blue-600: rgb(70, 147, 255);
--primitive-blue-700: rgb(0, 54, 129);
--primitive-blue-400: #4693ff;
--primitive-blue-200: rgb(185, 214, 255);
--primitive-overlay-dark: #262626;
--primitive-error: #b20f03;
--color-surface-overlay: var(--primitive-gray-200);
--color-text-inverse: var(--primitive-white);
--color-text-error: var(--primitive-error);
--color-cta-border-hover: var(--primitive-blue-700);
--color-cta-text-hover: var(--primitive-white);
--color-link-overlay-hover: var(--primitive-overlay-dark);
--color-link-overlay-error: var(--primitive-error);
--color-banner-link-hover-light: var(--primitive-blue-700);
--color-banner-link-hover-dark: var(--primitive-blue-200);
--color-input-focus-ring: var(--primitive-blue-400);
--border-width-default: 1px;
--border-style-default: solid;
--border-color-default: var(--color-text-primary);
--border-focus: 2px solid var(--color-focus-ring);
--color-text-link: rgb(0, 0, 238);
--color-text-link-hover-dark: rgb(185, 214, 255);
--color-text-link-hover-light: rgb(0, 54, 129);
--color-surface: rgba(0, 0, 0, 0);
--color-action-primary-hover-bg: rgb(0, 54, 129);
--color-action-primary-hover-text: rgb(255, 255, 255);
--primitive-grey-950: rgb(49, 49, 49);
--primitive-grey-900: rgb(38, 38, 38);
--primitive-grey-000: rgb(255, 255, 255);
--primitive-transparent: rgba(0, 0, 0, 0);
--primitive-blue-legacy: rgb(0, 0, 238);
--primitive-red-800: rgb(178, 15, 3);
--primitive-grey-100: rgb(242, 242, 242);
--color-text-on-action: rgb(255, 255, 255);
--color-surface-default: rgba(0, 0, 0, 0);
--color-action-hover-bg: rgb(0, 54, 129);
--color-action-hover-text-secondary: rgb(38, 38, 38);
--link-color-default: rgb(0, 0, 238);
--link-color-hover-dark-context: rgb(185, 214, 255);
--link-color-hover-light-context: rgb(0, 54, 129);
--focus-outline-color: rgb(70, 147, 255);
--button-hover-bg: rgb(0, 54, 129);
--button-hover-color: rgb(255, 255, 255);
--border-none: none;
--border-width-focus: 2px;
--border-color-focus: rgb(70, 147, 255);
--motion-stroke: stroke-dashoffset → 0;
/* Typography (22) */
--font-size-xs: 12px; /* 3 elements — e.g. span "Performance and Secu", a "Cloudflare", a "Privacy" /* mined from computed styles */ */
--font-size-sm: 16px; /* 2 elements — e.g. p "This website uses a ", div "www.perplexity.ai" /* mined from computed styles */ */
--font-size-md: 24px; /* 1 element — e.g. h2 "Performing security " /* mined from computed styles */ */
--font-size-lg: 40px; /* 1 element — e.g. h1 "www.perplexity.ai" /* mined from computed styles */ */
--font-weight-regular: 400; /* 5 elements — e.g. p "This website uses a ", span "Performance and Secu", a "Cloudflare" /* mined from computed styles */ */
--font-weight-medium: 600; /* 2 elements — e.g. h1 "www.perplexity.ai", h2 "Performing security " /* mined from computed styles */ */
--line-height-tight: 18px; /* 3 elements — e.g. span "Performance and Secu", a "Cloudflare", a "Privacy" /* mined from computed styles */ */
--line-height-normal: 30px; /* 1 element — e.g. h2 "Performing security " /* mined from computed styles */ */
--line-height-loose: 50px; /* 1 element — e.g. h1 "www.perplexity.ai" /* mined from computed styles */ */
--font-family-sans: system-ui, -apple-system, …;
--font-family-mono: monospace;
--perplexity-font-size-xs: 12px;
--perplexity-font-size-sm: 16px;
--perplexity-font-size-md: 24px;
--perplexity-font-size-lg: 40px;
--perplexity-font-weight-regular: 400;
--perplexity-font-weight-medium: 600;
--perplexity-line-height-tight: 18px;
--perplexity-line-height-normal: 30px;
--perplexity-line-height-loose: 50px;
--font-sans: system-ui, -apple-system, ...;
--font-mono: monospace;
/* Spacing (21) */
--space-xs: 8px; /* 3 elements — e.g. div .footer-link-wrapper, div .footer-link-wrapper, div .footer-link-wrapper /* mined from computed styles */ */
--space-xs: 8px;
--space-sm: 16px;
--space-md: 24px;
--space-lg: 32px;
--space-xl: 40px;
--space-2xl: 48px;
--space-3xl: 64px;
--breakpoint-xs: 350px;
--breakpoint-md: 720px;
--breakpoint-lg: 1024px;
--container-max-width: 1024px;
--container-padding-inline: var(--space-md);
--perplexity-space-xs: 8px;
--focus-outline-offset: 2px;
--focus-outline-width: 2px;
--container-padding-mobile: var(--space-sm);
--container-padding-desktop: var(--space-lg);
--bp-sm: 350px;
--bp-md: 720px;
--bp-lg: 1024px;
/* Radius (4) */
--radius-focus: 2px;
--radius-none: 0px;
--focus-radius: 2px;
--focus-border-radius: 2px;
/* Effects (3) */
--shadow-none: none;
--shadow-sm: [TBD - extract manually];
--shadow-md: [TBD - extract manually];
/* Motion (12) */
----motion-spin: @keyframes spin {
100% { transform: rotate(360deg); }
}; /* @keyframes spin */
----motion-scale: @keyframes scale {
0%, 100% { transform: none; }
50% { transform: scale3d(1, 1, 1); }
}; /* @keyframes scale */
----motion-stroke: @keyframes stroke {
100% { stroke-dashoffset: 0; }
}; /* @keyframes stroke */
----motion-scale-up-center: @keyframes scale-up-center {
0% { transform: scale(0.01); }
100% { transform: scale(1); }
}; /* @keyframes scale-up-center */
----motion-fade-in: @keyframes fade-in {
0% { opacity: 0; }
100% { opacity: 1; }
}; /* @keyframes fade-in */
----motion-fireworks: @keyframes fireworks {
0% { transform: scale(0); opacity: 0; }
50% { transform: scale(1.5); opacity: 1; }
100% { transform: scale(2); opacity: 0; }
}; /* @keyframes fireworks */
----motion-firework: @keyframes firework {
0% { opacity: 0; stroke-dashoffset: 8; }
30% { opacity: 1; }
100% { stroke-dashoffset: -8; }
}; /* @keyframes firework */
----motion-unspin: @keyframes unspin {
40% { stroke-width: 1px; stroke-linecap: square; stroke-dashoffset: 192; }
100% { stroke-width: 0; }
}; /* @keyframes unspin */
----motion-fade-in-animation: @keyframes fade-in-animation {
0% { fill: rgb(178, 15, 3); stroke: rgb(178, 15, 3); }
100% { fill: rgb(242, 242, 242); stroke: rgb(242, 242, 242); }
}; /* @keyframes fade-in-animation */
----motion-dots: @keyframes dots {
0% { content: ""; }
25% { content: "."; }
50% { content: ".."; }
75% { content: "..."; }
100% { content: ""; }
}; /* @keyframes dots */
----motion-lds-ring: @keyframes lds-ring {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}; /* @keyframes lds-ring */
--ease-default: ease; /* 8 elements — e.g. a, a, a /* mined from computed styles */ */
```
## Appendix B: Token Source Metadata
```
tokenSource: reconstructed-from-computed
siteURL: perplexity.ai
extractionDate: [TBD — record date of extraction]
nativeCSSVars: 0
confidence: low
Curated token set: 11 tokens (--perplexity-* prefix)
- Typography: 9 tokens (font-size × 4, font-weight × 2, line-height × 3)
- Spacing: 1 token (--perplexity-space-xs: 8px)
- Motion: 1 token (--perplexity-ease-default: ease)
All 11 are HIGH confidence — sourced from computed style census of 3+ elements each.
Reconstructed tokens:
- Colour palette: 10 primitives + 9 semantics — MODERATE confidence
Clustering method: hue family grouping (blues, greys) + usage context
(link vs focus vs text vs button). Body class selectors (.MDYf7, .VMCj2)
provided the dark/light split.
- Spacing scale (--space-sm through --space-2xl): HIGH confidence for
sm/md/lg (confirmed against body margin-bottom=32px, h2 margin=8px,
body font-size=16px). LOW confidence for --space-2xl (inferred from 8px grid).
- Font stacks: HIGH confidence — direct extraction from computed fontFamily.
- Border-radius: 0px on all structural elements (HIGH confidence).
2px exclusively on focus outlines (HIGH confidence from CSS rule extraction).
- Box-shadow / elevation: NONE found — HIGH confidence the design is flat.
- Line-height discrepancy: body computed line-height = 24px (not in curated set).
--perplexity-line-height-normal = 30px (h2 only). Note both in usage.
- Container widths: LOW confidence — inferred from breakpoints only, not extracted.
- Duration (200ms): MODERATE confidence — standard value, not extracted from CSS.
Keyframe animations: 11 extracted — HIGH confidence on existence, LOW confidence
on usage context (no component-to-animation mapping available).
Body class variants: .MDYf7 (dark) and .VMCj2 (light) detected from interactive
state rules. Theme-switching mechanism not extracted.
Gaps / unknowns:
- Default button styles (non-hover state of .SWnb2) not extracted
- Visited link colour not captured
- Disabled state colours not captured
- Inline code background/padding not captured
- Container max-width not confirmed
- Z-index scale entirely reconstructed
Extract these manually from browser DevTools for production accuracy.
```More from the gallery
Browse all kits →You may also like

atlassian.com
MITEnterprise design system for Atlassian's marketing site, featuring bold brand colours, custom typography, and structured spacing tokens built for large-scale SaaS platforms
06
boldsaaslanding-pagelight

github.com
MITGitHub's production design system with 568 CSS custom properties, featuring clean typography, accessible colour tokens, and motion primitives built for developer platforms and collaborative workflows
04
darkdeveloper-toolsaas

Customer.io
MITDark SaaS design system with mint-green accents and clean typography, built for product dashboards and developer-focused platforms
011
darksaasdeveloper-tooldashboard