Skip to content
All skills

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 (@theme and :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 older text-[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-panel 20, z-controls 30, z-header and z-dropdown 40, z-full-page 45, z-mini-globe 46, z-floating 47, z-overlay 50, z-notice 60, z-skip-link 100. 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.ts keeps both in sync.
  • A colour JavaScript needs goes into palette.ts as well; palette.test.ts checks it equals the CSS token.
  • Adding a token: put it in @theme with a one-line comment saying where it is used. Reuse an existing token if the meaning is the same.

Colour roles

RoleTokensUse
Map layerspace, space-deep (page body), space-raised(-hover), bandGlobe background, glass, dark bands (regional profile header, dark sections)
Content layerwhite, paper, paper-soft, surface-muted, surface-hoverPanels, full pages, cards, admin tables
Textink (headings), ink-soft (body), ink-muted (secondary), ink-deep (on white pills over dark)All text on light surfaces
Linesline (dividers), field-border (form control borders)Separators, card borders, inputs
Accentaccent, accent-strong (hover), accent-soft (tints), accent-bright (on dark), linkOne primary action per view, links, focus rings
Patronspatron, patron-strong, patron-brightMembership and support only
Goldgold, gold-light, gold-deep, gold-edgeCoin, spark ring and other warm "support / invite" highlights
Statussuccess, warning, danger (+ -soft)Badges, results, errors
Emptyempty, empty-strong, empty-softOnly "this place has no topics yet" (red zero, NoTopicsCta, ContentStatusBadge)
Content progressstatus-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", Note tone="inverse", kickerVariants glass, glassVariants), not through new components.
  • Contrast facts for the common pairs are in reference. Short version: ink-muted is the lightest text allowed on white or paper; line is decorative only; field-border is the minimum for a control's outline; status-preparing and status-ready are for icons next to a text label, their -text twins are for text.

Typography

  • Two families only: Manrope (font-display) for titles and headings, Inter (font-sans) for everything else. Both load through next/font in src/app/layout.tsx.
  • Rich text always renders inside .prose-atlas (15 px, line height 1.75, h2 22 px, h3 17 px, underlined links in link); .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 in ArticleHero. 10.5 to 11 px only for uppercase kickers and photo credits.
  • Short lines under a heading use Note (EmptyNote, Lead: size xs|sm|md, tone soft|muted|inverse). Small uppercase labels use kickerVariants (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, isTrustedPhoto in src/lib/images.ts) through photoUrl / photoSrcSet at the matching PHOTO_WIDTH role: avatar, thumb, tile, card, hero. Sizes, formats and lazy loading: atlas-performance.
  • The hero photo is a real <img> in ArticleHero. Its alt text is the page title: owner decision, keep it.
  • Every photo shows its credit with PhotoCredit (onPhoto when 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 #rrggbb only).
  • 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 in atlas-content.

Icons

  • lucide-react only, 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 with navIcon(href) so menu and header match.
  • Topic tile icons come from TILE_ICONS (TileIcon); custom map icons from features/visuals/map/icon-names.ts.

Shape, depth and spacing

  • Radius: rounded-full for buttons, pills, chips and badges; rounded-lg for fields and small boxes; rounded-xl / rounded-2xl for 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 a border-line border instead.
  • Spacing is Tailwind's default 4 px scale. Full-page column: ARTICLE_BODY (max-w-6xl). Panel sections pad px-6 sm:px-10; pull them to the column edge with PANEL_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. Never transition-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 from GLOBE_COLORS and the plain light map of custom maps from PLAIN_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-soft or ink-muted on light, white tones on dark; pairs checked.
  • [ ] Red used for its one meaning (danger or empty), not both.
  • [ ] Font sizes from the existing steps; rich text inside .prose-atlas.
  • [ ] Photos through photoUrl, credited with PhotoCredit; 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 empty red for an error or danger red 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.

ForegroundBackgroundRatioAllowed for
ink-soft #474c63white8.5Body text
ink-muted #646a82white5.4Secondary text (lightest text colour on white)
ink-muted #646a82paper #f4f5f84.9Secondary text on paper pages
link #1f4fd8white6.6Links in prose
accent #3b4ce0white6.4Text, icons, white text on accent buttons
whitepatron #2563eb5.2Patron buttons
accent-bright #7f97ffspace #0a10207.0Accent text on the dark layer
patron-bright #60a5faspace #0a10207.5Headings on the dark band
field-border #8a90a6white3.2Outline of inputs and selects (meets 3:1 for UI parts)
line #c3c8d3white1.7Decorative dividers only; never the only outline of a control
empty #dc2626white4.8The red zero and its border
empty-strong #b91c1cempty-soft #fef2f25.9Text of the zero-topics invitation
status-ready-text #16794awhite5.4"Ready" label
status-preparing-text #9a6200white5.1"In preparation" label
status-ready #1e9e5awhite3.5Icon only (with a text label)
status-preparing #d98a00white2.8Icon only, always next to a text label; never text
success / warning / dangertheir -soft6.5 / 6.4 / 5.3Badges and result messages
land #7d8aa8 (GLOBE_COLORS.land)space-deep5.7Countries 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

KindCSSJS twinGuard
Colours@theme --color-*THEME_COLORS, PLAIN_MAP_COLORS, GLOBE_COLORS in src/config/palette.tspalette.test.ts
Fonts--font-display, --font-sansnext/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.tslayout.test.ts
Utilities@utility touch-hit, .glass, .glass-hover, .panel-scroll, .prose-atlas, .prose-compact, .spark-ring, .coin-faceglassVariants, kickerVariants—

Known debt (do not copy)

  • Many components use arbitrary values: text-[Npx] and text-[var(--color-…)]. New code uses the token utilities; old code is converted when touched.
  • The danger Button variant uses Tailwind's red-700 palette directly instead of a token.
  • The glass surfaces, the MapLibre overrides and the topic pulse hold rgba() literals in globals.css; that file is the one place where raw values are allowed.

Visual variants that already exist

ComponentVariants
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
kickerVariantstone pill, muted, glass
glassVariantskind panel, pill
Breadcrumbstone light, dark
Railtone 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

This site uses only the cookies it needs to work — no advertising or tracking cookies. Visits are counted without cookies. Privacy