Color, layout, type, elevation, motion, and icons. Values shipped by simple-shared-react, and the rules for using them.
Not only a way to show the brand, but a way to communicate. A palette built with purpose adds meaning to the interface.
Six 11-step scales defined in base.css and exposed as Tailwind utilities. Swatches for every step are on the style guide home page.
| Family | Name | Role | Anchor |
|---|---|---|---|
| blue | Simple Blue | Brand. Primary actions, navigation, headings on dark. | blue-600 |
| orange | Portland Orange | Accent. Calls to action, caveats, estimates. | orange-500 |
| teal | Keppel | Data accent. Charts and visualization. | teal-600 |
| gray | Gray | Text, borders, surfaces. Cool-tinted to sit with the blue. | gray-700 |
| green | Green | Success and verified states. Tailwind default. | green-600 |
| red | Red | Errors and danger. Tailwind default. | red-600 |
See the full scales on the Brand page.
Accessible pairings between the primary colors, with background and text at an AA contrast ratio or better. Ratios below are computed from the palette hex values.
| Use | Pair | Sample | Contrast |
|---|---|---|---|
| Body text (p) | gray-700 on white | Aa | 4.30:1 Fail |
| Headings (h1) | gray-900 on white | Aa | 7.61:1 AAA |
| Small print, h6 | gray-500 on white | Aa | 2.56:1 Fail |
| Links (a) | blue-400 on white | Aa | 2.52:1 Fail |
| Inline code | blue-700 on gray-100 | Aa | 6.16:1 AA |
| Primary button | white on blue-600 | Aa | 4.42:1 Fail |
| Accent button | white on orange-500 | Aa | 3.12:1 Fail |
| Hero, footer | white on blue-900 | Aa | 10.52:1 AAA |
| Blue tint panel | blue-900 on blue-50 | Aa | 9.80:1 AAA |
| Orange callout | orange-800 on orange-50 | Aa | 6.69:1 AA |
| Teal callout | teal-800 on teal-50 | Aa | 7.10:1 AAA |
| Dark mode body | gray-300 on gray-900 | Aa | 4.94:1 AA |
| Dark mode headings | gray-100 on gray-900 | Aa | 6.64:1 AAA |
Besides the brand colors, roles for disabled states, backgrounds, actions, and high-contrast text. Today components pick raw scale steps; naming the roles is the first step to tokens.
| Role | Tokens | Sample | Use for |
|---|---|---|---|
| Brand / primary action | blue-600 | Aa | Primary buttons, active navigation, selected states. |
| Accent | orange-500 | Aa | One accent per screen: a call to action, a highlight, the eyebrow dot. |
| Information | blue-50 / blue-900 | Aa | Context and explanations. Blue callouts and tags. |
| Success | green-50 / green-800 | Aa | Completed actions, verified data, in network. |
| Warning / estimate | orange-50 / orange-800 | Aa | Caveats, estimates, anything the reader should double-check. |
| Danger | red-50 / red-700 | Aa | Destructive actions, validation errors, out of network. |
| Neutral | gray-100 / gray-700 | Aa | Disabled states, pending, secondary information. |
| Data accent | teal-500 | Aa | Charts and data visualization only, so it never competes with status colors. |
Next step: define these as --color-* aliases in base.css (for example--color-primary: var(--color-blue-600)) so components reference the role, not the step.
A dark version of the palette so the system can adapt to OS color preferences. The plumbing exists; no deployed product switches it on yet.
Dark mode is class-based, not media-query based: @custom-variant dark (&:where(.dark, .dark *)). Adding .dark to <html> switches every dark: utility. The element styles and shared components all carry dark variants.
| Surface | Light | Dark |
|---|---|---|
| Page | gray-100 | gray-900 |
| Card | white | black/50 |
| Input | white | black/50 |
| Dropdown, popover | white | gray-950 |
| Body text | gray-700 | gray-300 |
| Headings | gray-900 | gray-100 |
| Border | gray-200 | gray-800 |
Open questions: whether to follow prefers-color-scheme automatically, and where a manual toggle lives. Until then, treat dark variants as required on every new component so the switch is a one-line change later.
How and when to use the palette, what to keep in mind, and how not to use it.
Consistent use of a grid and spacing makes the interface easier to scan and the content easier to grasp.
A 4-point system. Tailwind's spacing scale is 0.25rem per step, so every spacing, sizing, and gap utility lands on a multiple of 4px.
14px28px312px416px520px624px832px1040px1248px1664px2080px2496pxHalf steps (0.5, 1.5, 2.5) are allowed for optical adjustments inside a component, never for layout between components.
Every layout sits on a grid that brings order to the interface. Today layouts use CSS grid utilities and a set of container widths; the widths are the grid.
| Container | Class | Width | Use for |
|---|---|---|---|
| Reading column | max-w-2xl | 672px | Prose, leads, anything meant to be read line by line. |
| Hero text | max-w-3xl | 768px | Centered page titles and leads. |
| Page content | max-w-4xl | 896px | Guide pages, forms, settings. |
| Wide content | max-w-6xl | 1152px | Footer, data tables, dashboards, search results. |
Twelve columns at lg and up, six at md, four below. Express withgrid-cols-* and col-span-*; there is no fixed column component. Gutters are gap-4 or gap-6, matching the spacing scale.
Page gutters are px-6 (24px) and px-8 from sm. Containers center with mx-auto. Nothing gets a min-widthwider than the smallest breakpoint.
The screen sizes the grid adapts to. Tailwind defaults, mobile-first: each prefix applies at that width and above.
| Prefix | Min width | Typical device |
|---|---|---|
sm | 640px | Large phones, landscape |
md | 768px | Tablets |
lg | 1024px | Small laptops |
xl | 1280px | Desktops |
2xl | 1536px | Wide desktops |
Design for the base (no prefix) first. Most layouts need only sm and md; reach for lg and up for multi-column data views.
Straightforward rules for space between elements, independent of the grid. Use gap on flex and grid parents rather than margins on children.
| Between | Class | Size |
|---|---|---|
| Icon and its label | gap-1.5 / gap-2 | 6–8px |
| Controls in a row (buttons, tags) | gap-3 | 12px |
| Fields in a form | gap-4 | 16px |
| Cards in a grid | gap-4 / gap-6 | 16–24px |
| Blocks within a section | gap-8 | 32px |
| Sections on a page | gap-16 | 64px |
| Page gutter | px-6 sm:px-8 | 24px, 32px from sm |
| Card padding | p-5 / p-6 | 20–24px |
Corner rounding signals what kind of surface an element is: tight on controls, softer on cards, softest on things that float. Tailwind's radius scale; the assignments are what the shared components use today.
xs2pxsm4pxmd6pxlg8pxxl12px2xl16pxfull9999px| Step | Class | Size | Use for |
|---|---|---|---|
xs | rounded-xs | 2px | Nothing today. Reserved for hairline details like checkbox boxes. |
sm | rounded / rounded-sm | 4px | Controls: buttons, inputs, selects, tags, inline code, callouts. The default. |
md | rounded-md | 6px | Cards. The shared Card component. |
lg | rounded-lg | 8px | Floating surfaces: modals, toasts, popovers, tabs, skeleton blocks. |
xl | rounded-xl | 12px | Style guide tiles and panels. Not used in the shared components. |
2xl | rounded-2xl | 16px | Marketing and hero surfaces only. |
full | rounded-full | 9999px | Pills and circles: toggles, avatars, icon buttons, spinners, range thumbs. |
| Component | Class | Size |
|---|---|---|
| Button, Callout, Dropdown trigger and menu, input, code | rounded | 4px |
| Tag | rounded-sm | 4px |
| Card | rounded-md | 6px |
| Modal, Toast, Info popover, Tabs, LoadingSkeleton | rounded-lg | 8px |
| Toggle, Range thumb, Loading spinner, Modal close, Toast icon | rounded-full | 9999px |
The gap: Card sits at md (6px), between the controls it holds and the modals that float above it, and Tag uses rounded-sm where everything else at 4px uses bare rounded. Proposed: keep three working sizes, sm for controls, lg for containers, full for pills, and move Card to lg so cards and modals share a radius. Tag stays at 4px; only the class name changes.
One of the main ways content is surfaced. Clear hierarchy and contrasting styles make things easier to read and give the brand its presence.
Manrope for interface text, Noto Sans Mono for code and data. Styles are applied per element in base.css, so semantic HTML gets the right type for free.
| Element | Size | Line height | Weight | Tracking | Color |
|---|---|---|---|---|---|
h1 | 24px | 1.25 (30px) | 700 | -0.025em | gray-900 |
h2 | 20px | 1.375 (27.5px) | 600 | -0.025em | gray-800 |
h3 | 16px | 1.375 (22px) | 600 | 0 | gray-800 |
h4 | 14px | 1.5 (21px) | 600 | 0 | gray-700 |
h5 | 12px | 1.5 (18px) | 700, uppercase | 0.025em | gray-600 |
h6 | 12px | 1.5 (18px) | 500, uppercase | 0.05em | gray-500 |
p, li | 14px | 1.625 (22.75px) | 400 | 0 | gray-700 |
small | 12px | 1.5 (18px) | 400 | 0 | gray-500 |
code | 12px mono | 1.5 | 400 | 0 | blue-700 on gray-100 |
label | 14px | 1.5 | 500 | 0 | gray-600 |
A scale that adapts to the viewport for a more meaningful hierarchy. Body text and headings are fixed today; only hero titles step up.
Element styles are one size at every width. Page heroes override totext-3xl md:text-5xl. Body copy stays 14px everywhere, which reads well on phones and slightly small on wide desktops.
Step h1 and h2 up one size from md(md:text-3xl, md:text-2xl). Leave body text alone; product screens are data-dense and 14px is deliberate.
Font sizes and leading should match the grid so text pairs cleanly with icons and controls.
Tailwind sizes pair each font size with a line height on the 4px grid: 12/16, 14/20, 16/24, 20/28, 24/32. Icon sizes (16, 20, 28, 36) are chosen to sit on those line heights, so anIcon size="sm" next to 14px text centers without adjustment. The relaxed leading on p (1.625) breaks the grid deliberately in favor of readability.
Tracking, leading, and line length tuned for reading.
| Property | Value | Why |
|---|---|---|
| Line length | max-w-2xl | About 75 characters at 14px Manrope. Prose wider than this is hard to track from line to line. |
| Leading | leading-relaxed | 1.625 on body copy. Manrope has a large x-height and needs the room. |
| Tracking | tracking-tight | Only on h1 and h2, where Manrope loosens at larger sizes. Never tighten body text. |
| Uppercase | tracking-wide | Uppercase labels (h5, h6, tags, eyebrows) always get extra tracking. |
| Numbers | tabular-nums | Proposed for any column of prices so digits align. Manrope supports it. |
Custom fonts must load before they display. Sensible fallbacks and fast loading keep text from flashing.
Both faces load through next/font/google, which downloads them at build time and serves them from the app. No request ever goes to Google at runtime.
Latin only. Manrope ships as a variable font, so every weight comes in one file.
next/font generates a size-adjusted system fallback and usesfont-display: swap, so text renders immediately and reflows minimally.
How and when to use the scale, and how not to.
The relative distance between surfaces on the z-axis. Shadows carry it in light mode; background color carries it in dark mode.
Shadow values for each supported elevation level. Five levels cover every shared component today; the names are new, the values are what the components use.
| Level | Name | Shadow | Preview | Use for |
|---|---|---|---|---|
| 0 | Flat | none | Page sections, tables, list rows. Borders do the separating. | |
| 1 | Raised | shadow-sm | Inputs, panels, anything resting on the page. | |
| 2 | Card | shadow-md shadow-gray-500/20 | Cards. The shared Card component. | |
| 3 | Floating | shadow-lg/15 | Dropdowns, popovers, tooltips. Anchored to a trigger. | |
| 4 | Overlay | shadow-xl | Modals and dialogs. Blocks the page beneath. |
Each elevation level has a linked background. In light mode they all resolve to white; in dark mode they replace the shadow.
| Level | Light | Dark |
|---|---|---|
| 0 Flat | gray-100 | gray-900 |
| 1 Raised | white | black/50 |
| 2 Card | white | black/50 |
| 3 Floating | white | gray-950 |
| 4 Overlay | white | gray-950 (proposed) |
Dark mode currently reuses black/50 for levels 1 and 2, so cards and inputs read at the same height. Proposed: lighten level 2 to gray-925 so cards lift off inputs.
A system of z-index values that controls which elements render on top. Every overlay in the shared library uses z-50 today, so stacking order is decided by DOM order.
| Layer | Proposed | Today |
|---|---|---|
| Sticky content (table headers, section nav) | z-10 | unset |
| Dropdowns, popovers, tooltips | z-20 | z-50 |
| Fixed header | z-30 | z-50 |
| Modal scrim and dialog | z-40 | z-50 |
| Toasts | z-50 | z-50 |
The visible bug this fixes: a dropdown opened inside a modal can render behind a toast, and a fixed header can cover a dropdown that opens above its trigger.
Shared motion values give a coherent experience and keep animation aligned with the brand. All animation uses motion/react; no CSS keyframes or Tailwind animate-* utilities.
Standard easing functions for component transitions: standard, accelerated, and decelerated cover the common cases.
| Name | Value | Use for |
|---|---|---|
| Enter | easeOut | Things appearing: dropdowns, popovers, toasts, modals. Fast start, gentle landing. |
| Exit | easeIn | Things leaving. Mirrors enter. (Shared components currently reuse easeOut on exit.) |
| Standard | ease-in-out | Property changes in place: color, transform, rotation. Tailwind transition default. |
| Linear | linear | Continuous motion only: spinners, progress. |
A small set of durations keeps transitions consistent across products.
| Name | Value | Use for |
|---|---|---|
| Instant | 100ms | Hover and focus color changes. |
| Fast | 200ms | Dropdowns, popovers, chevron rotation, transition-colors. The most common value in the shared components. |
| Normal | 300ms | Toasts, modals, anything moving more than a few pixels. |
| Slow | 500ms | Page-level transitions and reveals. Rare. |
| Component | Initial state | Scrim |
|---|---|---|
| Modal | opacity 0, scale 0.95, y 20 | opacity 0 → 1 |
| Dropdown | opacity 0, y ∓10 (toward trigger) | — |
| Info popover | opacity 0, scale 0.95, y 10 | — |
| Toast | opacity 0, scale 0.95, y 50 | — |
The pattern is consistent: fade in, scale from 0.95, and slide a short distance from the direction the element comes from. Exit reverses it. New components should copy it rather than invent a new one.
Respect the reduced-motion preference by making animations less prominent or removing them.
prefers-reduced-motion.Wrap each app in <MotionConfig reducedMotion="user"> frommotion/react. It disables transform and layout animations for users who ask for less motion while keeping opacity fades, which is exactly the split the enter transitions above need. Spinners should switch to a static icon viauseReducedMotion().
Icons represent functionality or content at a glance. They reach their potential when used meaningfully and consistently.
The icon family should make visual sense as a whole. The set mixes two sources today.
Most of the set. Solid glyphs drawn with fill="currentColor" on a 32-unit viewBox. This is the house style.
About fifteen utility glyphs (chevrons, plus, X, search, sort) drawn with a 2px stroke on a 24-unit viewBox. They read lighter next to the filled set.
Decision needed: redraw the stroked glyphs as filled to match, or accept a deliberate split where filled icons represent things and stroked icons represent controls. Either is fine; mixing by accident is not.
Functional icons need an accessible name that says what they do. Decorative icons need none.
Icon component takes icon, size, and className. It renders a bare <svg> with no role, aria-label, or aria-hidden, so screen readers either skip it or read nothing useful.Add an optional label prop. With a label, renderrole="img" aria-label="...". Without one, renderaria-hidden="true". An icon-only button then passes its label to the icon or sets its own aria-label.
Name icons for what they communicate, not how they look. A play button is Play, not Triangle.
Names are PascalCase keys on the Icons map and mostly follow the rule:Search, Trash, LogOut. A few are named for their shape or source rather than their job and should be aliased: Funnel (filter),Asclepius (medical), Outbound (external link). Keep the old key, add the purpose name, and migrate over time.
Icons draw in a bounding box that plays well with the grid so they pair cleanly with text.
| Size | Rendered | Pairs with | Preview |
|---|---|---|---|
sm | 16px | Inline with 14px text, tags, small buttons | |
md | 20px | Default. Buttons, inputs, list rows | |
lg | 28px | Section headers, empty states | |
xl | 36px | Feature callouts, hero |
The component strips the SVG width and height and sizes by class, so any icon renders at any of the four sizes. All four land on the 4px grid.
Keywords make each icon discoverable for anyone using the system.
There is no icon browser and no keyword metadata. When the set grows past what fits on one screen, add a keywords list per icon and a searchable gallery to this style guide. Until then the components page and the Icons map are the catalog.
Icons for common actions reserved so they are never used for anything else. This makes the interface predictable.
XTrashPencilPlusSearchFunnelChevronDownInfoTriangleAlertCircleCheckCircleXOutboundHow and when to use icons, and how not to.