Visual design
Claude skill atlas-visual-design · for Designer, Frontend
The look of Atlas of Today's World - design tokens in globals.css @theme and src/config/layout.ts, colour roles (dark map layer, light content layer, semantic and red "empty" colours), typography scale, photos and credits, lucide icons, radius, shadow and spacing, and visual variants instead of copies. Use when choosing a colour, font size, icon, image, border, shadow or spacing, adding a token, or styling any new component or page.
Atlas has two layers: the dark map layer (night-sky globe, glass controls, dark bands) and the light content layer (white panels, paper pages, admin). Everything is built from the tokens below. A new look is a new token or a new variant, never a hand-picked value.
Before you start
- Tokens:
src/app/globals.css(@themeand:root). Layout numbers:src/config/layout.ts. Colours for code that cannot read CSS (MapLibre, canvas, OG images, manifest):src/config/palette.ts. - See every shared component and its variants in Storybook:
npm run storybook(port 6006). - Related skills:
atlas-accessibility(contrast bar, alt text, motion),atlas-maps-data(map and chart colour scales),atlas-frontend(how to implement a variant),atlas-ux(which block goes where),atlas-content(image licences and credits).
Rules
Tokens
- Colours come only from
--color-*tokens. Never write a hex value,rgb()or a Tailwind palette colour (bg-red-600) in a component. - In new code use the generated utilities:
bg-paper,text-ink-soft,border-line,rounded-panel,z-header. The oldertext-[var(--color-ink-soft)]form means the same and is known debt; convert it when you touch the line, not in unrelated PRs. - Layer order only through the z-index scale:
z-panel20,z-controls30,z-headerandz-dropdown40,z-full-page45,z-mini-globe46,z-floating47,z-overlay50,z-notice60,z-skip-link100. Plain numbers (z-10,-z-10) only inside one component. - Panel widths, the mini globe, the 768 px breakpoint and the 44 px touch size come from
layout.ts(RAIL,RAIL_WIDE,MINI_GLOBE,DESKTOP_MIN_PX,TOUCH_MIN_PX) and their CSS twins (--rail-width,--touch-min…).layout.test.tskeeps both in sync. - A colour JavaScript needs goes into
palette.tsas well;palette.test.tschecks it equals the CSS token. - Adding a token: put it in
@themewith a one-line comment saying where it is used. Reuse an existing token if the meaning is the same.
Colour roles
| Role | Tokens | Use |
|---|---|---|
| Map layer | space, space-deep (page body), space-raised(-hover), band | Globe background, glass, dark bands (regional profile header, dark sections) |
| Content layer | white, paper, paper-soft, surface-muted, surface-hover | Panels, full pages, cards, admin tables |
| Text | ink (headings), ink-soft (body), ink-muted (secondary), ink-deep (on white pills over dark) | All text on light surfaces |
| Lines | line (dividers), field-border (form control borders) | Separators, card borders, inputs |
| Accent | accent, accent-strong (hover), accent-soft (tints), accent-bright (on dark), link | One primary action per view, links, focus rings |
| Patrons | patron, patron-strong, patron-bright | Membership and support only |
| Gold | gold, gold-light, gold-deep, gold-edge | Coin, spark ring and other warm "support / invite" highlights |
| Status | success, warning, danger (+ -soft) | Badges, results, errors |
| Empty | empty, empty-strong, empty-soft | Only "this place has no topics yet" (red zero, NoTopicsCta, ContentStatusBadge) |
| Content progress | status-ready(-text), status-preparing(-text) | Ready (green check) and in preparation (amber hourglass) marks |
- Red has two meanings and they never mix:
danger= something went wrong or is destructive;empty= an invitation to write the first topic. - Keep the accent rare so the primary action stands out. Hover, active and focus states are stronger than the resting state.
- Dark variants come through props (
tone="dark", Notetone="inverse",kickerVariantsglass,glassVariants), not through new components. - Contrast facts for the common pairs are in reference.
Short version:
ink-mutedis the lightest text allowed on white or paper;lineis decorative only;field-borderis the minimum for a control's outline;status-preparingandstatus-readyare for icons next to a text label, their-texttwins are for text.
Typography
- Two families only: Manrope (
font-display) for titles and headings, Inter (font-sans) for everything else. Both load throughnext/fontinsrc/app/layout.tsx. - Rich text always renders inside
.prose-atlas(15 px, line height 1.75, h2 22 px, h3 17 px, underlined links inlink);.prose-compact(13 px) at card size. Do not style rich text ad hoc. - Use the existing steps, add none: 12, 12.5, 13, 13.5, 14 px for UI text; 15 px prose;
17 to 18 px small headings and dialog titles; 22 to 26 px section headings (admin h1 is 24 px);
26 to 34 px page titles; 34 px (52 px from
sm) for the hero title inArticleHero. 10.5 to 11 px only for uppercase kickers and photo credits. - Short lines under a heading use
Note(EmptyNote,Lead: sizexs|sm|md, tonesoft|muted|inverse). Small uppercase labels usekickerVariants(letter spacing 0.1 to 0.12 em, one line). - Numbers compared in a column (tables, stats, rankings) get
tabular-nums. - Use real typographic characters: … – — “ ” and a non-breaking space between a number and its unit.
Imagery
- Photos come only from trusted origins (
PHOTO_ORIGINS,isTrustedPhotoinsrc/lib/images.ts) throughphotoUrl/photoSrcSetat the matchingPHOTO_WIDTHrole: avatar, thumb, tile, card, hero. Sizes, formats and lazy loading:atlas-performance. - The hero photo is a real
<img>inArticleHero. Its alt text is the page title: owner decision, keep it. - Every photo shows its credit with
PhotoCredit(onPhotowhen it sits on the image). What the credit says and which licences are allowed:atlas-content. - Without a photo, a header uses the place's own colour:
heroWash/heroStyle(plain#rrggbbonly). - Text over a photo always has a dark gradient under it and uses the dark tones.
- Visual cards show the real map or chart, not stock art.
- Alt text: technical rules in
atlas-accessibility, wording inatlas-content.
Icons
lucide-reactonly, 16 to 20 px (size-4,size-5,size={18}). No emoji, no icon fonts, no hand-drawn SVG when lucide has the glyph.- An admin section has one icon, defined in
ADMIN_NAV; pages read it withnavIcon(href)so menu and header match. - Topic tile icons come from
TILE_ICONS(TileIcon); custom map icons fromfeatures/visuals/map/icon-names.ts.
Shape, depth and spacing
- Radius:
rounded-fullfor buttons, pills, chips and badges;rounded-lgfor fields and small boxes;rounded-xl/rounded-2xlfor cards, form sections and dialogs;rounded-panel(18 px) for glass panels over the map. A child's radius is never larger than its parent's. - Shadows only on things that float (glass panels
shadow-2xl shadow-black/40, popovers, floating buttons). Cards on a page use aborder-lineborder instead. - Spacing is Tailwind's default 4 px scale. Full-page column:
ARTICLE_BODY(max-w-6xl). Panel sections padpx-6 sm:px-10; pull them to the column edge withPANEL_SECTION_BLEED. - Over the map, translucent surfaces are
.glass/glassVariants(panel,pill), never a custom semi-transparent box.
Motion
- Which properties may animate (transform, opacity):
atlas-performance. Nevertransition-all. - Motion explains cause and effect: the globe flies to a place (
GLOBE.flightMs), a full page rises over it (full-page-in). - Keep the owner's animations:
coin-spin,coin-shine,spark-orbit,star-twinkle,topic-pulse. Reduced-motion handling:atlas-accessibility.
Charts and maps (basics)
- Colour scales, category limits, legends and chart choice live in
atlas-maps-data. In code, globe colours come fromGLOBE_COLORSand the plain light map of custom maps fromPLAIN_MAP_COLORS(src/config/palette.ts), never from new literals. - A place, region or global issue keeps the same colour wherever it appears (globe, cards,
headers via
heroWash).
How we do it in Atlas
// A new look is a variant of the closest component, not a copy.
<Button variant="patron">Become a patron</Button>
<Badge tone="warning">Scheduled</Badge>
<p className={kickerVariants({ tone: "glass" })}>Latest subtopics</p>
<EmptyNote size="sm" tone="muted">No topics match these filters.</EmptyNote>
<div className={glassVariants({ kind: "panel" })}>…</div>Checklist
- [ ] No hex,
rgb()or Tailwind palette colour in components; new tokens have a comment. - [ ] Text uses
ink,ink-softorink-mutedon light, white tones on dark; pairs checked. - [ ] Red used for its one meaning (
dangerorempty), not both. - [ ] Font sizes from the existing steps; rich text inside
.prose-atlas. - [ ] Photos through
photoUrl, credited withPhotoCredit; hero alt left as the title. - [ ] Icons from lucide; radius and shadow follow the table above.
- [ ] Storybook story shows the new variant in light and dark tone.
Anti-patterns
text-[13px] text-[#555]in a new component: bypasses the tokens and fails contrast later.- Using
emptyred for an error ordangerred for a missing topic: readers learn the wrong meaning. - A third font or a new size step "just here": the scale fragments quickly.
- A shadow on every card: depth stops meaning "floating".
- Copying a button's classes to make a slightly different button: add a variant instead.
Sources
Full list: reference.
- Tailwind CSS 4 theme variables: https://tailwindcss.com/docs/theme
- Vercel Web Interface Guidelines (contrast of states, radius, shadows, motion): https://vercel.com/design/guidelines
- Butterick, Practical Typography key rules: https://practicaltypography.com/summary-of-key-rules.html
- Linear UI redesign (few tokens, rare accent): https://linear.app/now/how-we-redesigned-the-linear-ui
- Datawrapper, colours in data visualisation: https://www.datawrapper.de/academy/what-to-consider-when-choosing-colors-for-data-visualization
Files of this skill
reference.md
atlas-visual-design: reference
Details behind SKILL.md.
Contrast of token pairs
WCAG 2.x ratios, computed from the hex values in src/app/globals.css (2026-10-16). The bar
itself (4.5:1 text, 3:1 large text and UI parts) is defined in atlas-accessibility.
| Foreground | Background | Ratio | Allowed for |
|---|---|---|---|
ink-soft #474c63 | white | 8.5 | Body text |
ink-muted #646a82 | white | 5.4 | Secondary text (lightest text colour on white) |
ink-muted #646a82 | paper #f4f5f8 | 4.9 | Secondary text on paper pages |
link #1f4fd8 | white | 6.6 | Links in prose |
accent #3b4ce0 | white | 6.4 | Text, icons, white text on accent buttons |
| white | patron #2563eb | 5.2 | Patron buttons |
accent-bright #7f97ff | space #0a1020 | 7.0 | Accent text on the dark layer |
patron-bright #60a5fa | space #0a1020 | 7.5 | Headings on the dark band |
field-border #8a90a6 | white | 3.2 | Outline of inputs and selects (meets 3:1 for UI parts) |
line #c3c8d3 | white | 1.7 | Decorative dividers only; never the only outline of a control |
empty #dc2626 | white | 4.8 | The red zero and its border |
empty-strong #b91c1c | empty-soft #fef2f2 | 5.9 | Text of the zero-topics invitation |
status-ready-text #16794a | white | 5.4 | "Ready" label |
status-preparing-text #9a6200 | white | 5.1 | "In preparation" label |
status-ready #1e9e5a | white | 3.5 | Icon only (with a text label) |
status-preparing #d98a00 | white | 2.8 | Icon only, always next to a text label; never text |
success / warning / danger | their -soft | 6.5 / 6.4 / 5.3 | Badges and result messages |
land #7d8aa8 (GLOBE_COLORS.land) | space-deep | 5.7 | Countries on the globe |
Re-run the numbers when a token changes (any WCAG contrast checker, or the formula in https://www.w3.org/WAI/WCAG22/Understanding/contrast-minimum.html).
Where the tokens live
| Kind | CSS | JS twin | Guard |
|---|---|---|---|
| Colours | @theme --color-* | THEME_COLORS, PLAIN_MAP_COLORS, GLOBE_COLORS in src/config/palette.ts | palette.test.ts |
| Fonts | --font-display, --font-sans | next/font/google in src/app/layout.tsx | — |
| Radius | --radius-panel | — | — |
| Stacking | --z-index-* | — | — |
| Layout | :root --rail-width, --rail-width-wide, --touch-min, --mini-globe-* | RAIL, RAIL_WIDE, TOUCH_MIN_PX, MINI_GLOBE in src/config/layout.ts | layout.test.ts |
| Utilities | @utility touch-hit, .glass, .glass-hover, .panel-scroll, .prose-atlas, .prose-compact, .spark-ring, .coin-face | glassVariants, kickerVariants | — |
Known debt (do not copy)
- Many components use arbitrary values:
text-[Npx]andtext-[var(--color-…)]. New code uses the token utilities; old code is converted when touched. - The
dangerButton variant uses Tailwind'sred-700palette directly instead of a token. - The glass surfaces, the MapLibre overrides and the topic pulse hold
rgba()literals inglobals.css; that file is the one place where raw values are allowed.
Visual variants that already exist
| Component | Variants |
|---|---|
Button / buttonVariants (components/ui/button.tsx) | variant: primary, outline, danger, patron, ink, ghost, quiet, quietDanger; size: md, sm, chip, icon, dense, denseIcon, rowIcon; block |
Badge (components/ui/badge.tsx) | tone: neutral, accent, success, warning, danger, outline; size sm, md |
Note (EmptyNote, Lead) | size xs, sm, md; tone soft, muted, inverse; leading |
kickerVariants | tone pill, muted, glass |
glassVariants | kind panel, pill |
Breadcrumbs | tone light, dark |
Rail | tone light, dark; layout scroll, grid, pages |
PhotoCard, PhotoTile (tileStyle) | card and tile styles |
Sources
- Tailwind CSS theme variables: https://tailwindcss.com/docs/theme
- Tailwind dark mode: https://tailwindcss.com/docs/dark-mode
- Vercel Web Interface Guidelines: https://vercel.com/design/guidelines
- Practical Typography, key rules: https://practicaltypography.com/summary-of-key-rules.html
- Baymard, line length: https://baymard.com/blog/line-length-readability
- GOV.UK type scale: https://design-system.service.gov.uk/styles/type-scale/
- Linear UI redesign: https://linear.app/now/how-we-redesigned-the-linear-ui
- Datawrapper, colours for data visualisation: https://www.datawrapper.de/academy/what-to-consider-when-choosing-colors-for-data-visualization
- FT Visual Vocabulary: https://github.com/Financial-Times/chart-doctor/blob/main/visual-vocabulary/README.md
- WCAG 2.2 contrast (minimum): https://www.w3.org/WAI/WCAG22/quickref/#contrast-minimum
- WCAG 2.2 non-text contrast: https://www.w3.org/WAI/WCAG22/quickref/#non-text-contrast