Design System
Carbon-inspired dark-only visual language. Token-based color, typography, spacing, and component rules for every UI surface in Slop.
Brand & Style
Slop is a dark-only desktop web app for running AI agent workers against issue queues. Its visual language is derived from IBM Carbon Design System: deep near-black backgrounds, neutral grays for secondary content, and a single purple accent for interactive affordances. Saturated color is rare and deliberate -- only status indicators and the brand mark break the grayscale base.
The brand mark uses vivid green #22c55e on the logo shape only. This is the sole appearance of that green in the product; it is never used as a button color, status tone, or decorative surface. Everything interactive defaults to purple #8b5cf6.
The system is dark-only. There is no light mode, no theme switch, and no media-query color swap. All surface layers step through four discrete background values from the page floor up to floating surfaces. Copy steps through three text values from primary down to tertiary.
Typography is set in Inter (variable sans) for UI copy and JetBrains Mono for log/code output. The scale is restrained -- most UI text sits at 13--15 px. Larger steps exist only for page <h1> headings (25 px).
Animation is minimal. Only the enter keyframe (opacity + translateY, 180 ms ease-out) is defined at the system level. It is automatically disabled for users who prefer reduced motion.
Colors
Background layers
The four background values form a stacking order. Each layer sits exactly one step above the one below. Compose surfaces top-to-bottom: page floor, page surface, card/elevated, overlay/popover.
#0f0f0f
#161616
#1e1e1e
#262626
| Token | Value | Usage |
|---|---|---|
bg-base | #0f0f0f | True page floor, rarely painted directly |
bg-surface | #161616 | <body> background, Card background |
bg-elevated | #1e1e1e | Nav header, secondary button fill, surface-muted fills |
bg-overlay | #262626 | Dropdown panels, popovers, context menus |
Text hierarchy
| Token | Value | Usage |
|---|---|---|
text-primary | #ededed | Primary body copy, headings, active tab labels |
text-secondary | #737373 | Secondary/muted copy, inactive tab labels, subtitles |
text-tertiary | #404040 | Placeholder text, deeply de-emphasized labels |
Borders
Two border values pair with the background layers. Use border-subtle for large-area dividers and border-default for card outlines and control borders.
| Token | Value | Usage |
|---|---|---|
border-subtle | #222222 | Nav header bottom border, scrollbar track |
border-default | #2a2a2a | Card border, button border, popover border |
Accent (interactive)
The accent family is purple. All interactive affordances use the accent at rest, accent-hover on hover, and accent-foreground (white) for text/icons drawn on top.
#8b5cf6
#7c3aed
#ffffff
Status palette
Status colors always come in fg/bg pairs. The fg value is the readable text color; the bg value is the dark tinted fill used for badge backgrounds and alert surfaces. Never use a bg value as text or an fg value as a large painted surface.
| Tone | fg value | bg value | When to use |
|---|---|---|---|
| success | #34d399 | #0a2816 | Merged, completed |
| info | #67e8f9 | #0a1f2e | In-progress, CI waiting, has PR |
| warn | #fcd34d | #2e2000 | Needs attention, paused, conflict |
| danger | #fc8181 | #3a0a0a | Failed, PR closed, errors |
| neutral | #737373 | #1e1e1e | Queued, cancelled, closed |
Brand color
#22c55e is reserved exclusively for the SlopMark logo SVG. It does not appear in buttons, badges, status indicators, or any other UI element.
Graph edge colors
The orchestrator graph uses #8b5cf6 (same as accent) for normal dependency edges and #fcd34d (same as warn-fg) for blocked-path edges.
Typography
Two font families are loaded via next/font and injected as CSS variables on <html>:
--font-inter-- variable Inter, used for all UI copy via--font-sans--font-jetbrains-mono-- JetBrains Mono, used for log/code output via--font-mono
Scale
| Step | Value | Line height | Usage |
|---|---|---|---|
text-xs | 13 px | default | Badges, button sm, footnotes |
text-sm | 13 px | default | Button md, tab labels, secondary body copy |
text-base | 15 px | default | Primary body copy |
text-lg | 17 px | default | Subheadings, card titles |
text-xl | 21 px | default | Large headings |
text-page-title | 25 px | 2 rem | Page <h1> via PageHeader |
text-section | 15 px | 1.25 rem | Section labels via SectionHeading |
text-xs and text-sm intentionally resolve to the same size (13 px). The distinction is semantic -- text-xs signals "smallest" intent; text-sm signals "standard small UI."
Conventions by use case
| Context | Font | Size | Weight |
|---|---|---|---|
Page <h1> | sans | text-page-title (25 px) | semibold (600) |
| Dialog title | sans | text-base (15 px) | semibold (600) |
| Section heading label | sans | text-section (15 px) | semibold (600), uppercase, tracking-wide |
| Card/subheading | sans | text-lg (17 px) | semibold (600) |
| Body copy | sans | text-base (15 px) | regular (400) |
| Secondary copy | sans | text-sm (13 px) | regular (400) |
| Tab label | sans | text-sm (13 px) | medium (500) |
| Badge label | sans | text-xs (13 px) | medium (500) |
| Log/code output | mono | text-sm (13 px) | regular (400) |
Layout & Spacing
Page structure
The app is desktop-first and centered. All page content and the nav header sit inside max-w-4xl containers with px-8 horizontal padding. The sticky header uses z-40 to sit above content while dialogs and portals use z-[9999].
<body> background is bg-surface (#161616). The base background bg-base (#0f0f0f) serves as the true floor visible only through gaps, never explicitly painted onto content areas.
Spacing scale
The base unit is 4 px. Tokens 1 through 10 step in 4 px increments.
| Token | Value | Common use |
|---|---|---|
spacing-1 | 4 px | Icon-to-label gap, tight inline gap |
spacing-2 | 8 px | Badge padding, button vertical padding sm |
spacing-3 | 12 px | Button horizontal padding sm, small gaps |
spacing-4 | 16 px | Standard content padding, list item gap |
spacing-5 | 20 px | Moderate section spacing |
spacing-6 | 24 px | Card padding, standard section spacing |
spacing-8 | 32 px | Page horizontal padding |
spacing-10 | 40 px | Generous section padding |
Named rhythm tokens
Three named spacing tokens enforce vertical rhythm at the macro level. Use these instead of ad-hoc margin values in page layouts.
| Token | Value | Usage |
|---|---|---|
spacing.section | 2 rem | Between major page sections (space-y-section) |
spacing.block | 1.5 rem | Page title <header> to first content block (mt-block) |
spacing.tight | 0.75 rem | Section heading to content below it (mt-tight) |
Elevation & Depth
The depth system uses two mechanisms in combination: background value stepping (darker = deeper) and box shadows (lighter = more raised).
Background stepping
Depth is communicated primarily through background layering. Moving from bg-base toward bg-overlay moves a surface visually upward. Cards sit at bg-surface, floating panels at bg-overlay.
Shadow tokens
| Token | Value | Usage |
|---|---|---|
shadow-control | 0 1px 2px rgb(0 0 0 / 0.35) | Resting button, tooltip, menu panel |
shadow-control-hover | 0 2px 8px rgb(0 0 0 / 0.45) | Button on hover |
shadow-node | 0 1px 3px rgb(0 0 0 / 0.4), 0 4px 12px rgb(0 0 0 / 0.25) | Graph node cards, dialog |
Buttons transition between shadow-control and shadow-control-hover on hover. Disabled buttons suppress shadows entirely.
Dialog backdrop
Modal dialogs use a rgba(0,0,0,0.6) backdrop via the native <dialog>::backdrop pseudo-element.
Shapes
Border radius scale
| Token | Value | Usage |
|---|---|---|
rounded-sm | 3 px | Very tight corners: small inset labels |
rounded-md | 6 px | Dropdown items, popover inner rows |
rounded-lg | 8 px | Cards, dialog, popovers, tooltips |
rounded-control | 8 px | All buttons (primary, secondary, danger, destructive) |
rounded-full | 9999 px | Badges, active tab underline indicator, scrollbar thumb |
rounded-control and rounded-lg resolve to the same 8 px but are named separately: rounded-control is the button system token, rounded-lg is the container system token. This keeps button shape changes isolated from card shape changes.
Components
Button
The Button component (src/app/_components/ui/button.tsx) is the single primitive for all interactive triggers. Five variants, two sizes, and an iconOnly prop for single-glyph controls.
| Variant | Background | Text | Hover |
|---|---|---|---|
primary | #8b5cf6 (accent) | white | #7c3aed fill |
secondary | #1e1e1e (bg-elevated) | text-primary | opacity 90% |
danger | #161616 (bg-surface) | #fc8181 (danger-fg) | danger-bg fill |
ghost-danger | transparent | #fc8181 (danger-fg) | opacity 70% |
destructive | #7f1d1d | #fca5a5 | opacity 90% |
Sizes: sm = px-2.5 py-1 text-xs · md = h-8 px-3.5 text-sm. When busy=true, children are replaced by busyLabel and a Spinner appears. All variants translate 1 px down on :active.
Badge
Base: inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-xs font-medium. Five semantic tones: neutral, info, success, warn, danger. Custom color prop activates a color-mix() tint treatment for arbitrary hex values (e.g. GitHub label colors).
StateBadge
Wraps Badge with a curated mapping of worker/daemon states to tones and Unicode glyphs. Each entry includes { label, tone, icon }. The icon is aria-hidden; the aria-label reads "Status: {label}".
| Tone | States |
|---|---|
| neutral | queued, claimed, cancelled, closed |
| info | implementing, verifying, waiting_ci, waiting_review, in_review, waiting_merge, merging, reporting, has_pr |
| warn | waiting_address, in_address, paused, resolving_conflict, fixing_ci, refining, reviewing |
| success | merged, reviewed, refined |
| danger | failed, pr_closed |
Card
Zero-padding container: rounded-lg border border-border bg-surface. Consumers add internal padding via className. No shadow by default -- depth is communicated by bg-surface sitting above the bg-base page floor.
Navigation header
Sticky top nav with bg-elevated background and border-subtle bottom border. Contains: SlopMark SVG logo, five tab links (Board/Plan/Workers/Agents/Docs), and right controls (RepoSwitcher, HarnessDropdown, ConfigLink). Active tab gets a h-0.5 rounded-full bg-accent underline bar.
PageHeader
Standard page title block: optional back link, <h1> at text-2xl font-semibold (25 px/600), optional actions slot floated right, optional subtitle in text-sm text-text-muted.
SectionHeading
Renders an <h2> with text-section font-semibold uppercase tracking-wide text-text-muted. Use for labeled groups within a page. The uppercase + wide tracking makes section labels visually distinct from body <h2> content headings.
Tooltip
Portal-based plain tooltip. Panel: rounded border border-border bg-surface px-2 py-1 text-xs text-text-muted shadow-control. Hover-only. Never used as a primary information surface.
Menu (overflow menu)
A ⋯ secondary button trigger and a portal-rendered role="menu" panel. Dismisses on Escape and outside pointer click. MenuItem supports a separated divider prop for destructive items at the bottom.
ConfirmationDialog
Uses the native <dialog> element with .showModal(). Container: rounded-lg border border-border bg-surface p-6 shadow-node. Backdrop: rgba(0,0,0,0.6). Focus management: saves trigger element focus on open, restores on close.
Skeleton
Loading placeholder: animate-pulse rounded bg-surface-muted. Consumers control dimensions. Always aria-hidden. Size it to match the content it replaces -- not generic full-width bars.
Spinner
24x24 SVG circle that rotates via animate-spin. Default size 16x16. When embedded in Button's busy state, the surrounding span is aria-hidden to avoid duplicate announcements.
EmptyState
Centered dashed-border placeholder: rounded-lg border border-dashed border-border bg-surface-muted/40 px-6 py-12 text-center. Contains a title, optional description, and optional action slot. Keep empty state copy short and factual.
ErrorState
Alert surface for data load failures: role="alert" rounded-md border border-[status-error] bg-danger-bg px-6 py-12 text-center. Title in text-danger-fg, description in text-text-muted, optional retry button.
Graph nodes (orchestrator)
Node cards on the /graph view use shadow-node for elevation. Node header bars reuse the status fg/bg pair matching the node's current state. Graph edges use purple (accent) for normal paths and warn-yellow for blocked paths.
Scrollbar
Custom webkit scrollbar: 6 px width, border-subtle track, border-default thumb, rounded-full thumb radius. Matches the border color family so it recedes visually.
Do's and Don'ts
Use semantic token names (text-text, bg-surface, text-danger-fg) instead of raw hex literals. Token changes propagate everywhere automatically.
Use the fg/bg pair together. Never use a status -bg as text or a status -fg as a large painted background.
Use accent purple for all interactive affordances: links, focus indicators, primary buttons, active tab underlines.
Use rounded-control (8 px) for buttons and rounded-lg (8 px) for containers separately, even though they resolve to the same value.
Include an aria-label on icon-only buttons. Use iconOnly prop on Button to get square padding.
Use text-page-title for page <h1> and SectionHeading for labeled groups.
Add light-mode color variants. The system is dark-only -- no dark: prefix in Tailwind classes, no prefers-color-scheme: light branches.
Paint bg-base (#0f0f0f) on content surfaces. It is the true floor, visible only as a gap or through a translucent overlay.
Use text-tertiary for interactive or informational text. At that contrast level it is decorative only.
Use saturated or vivid colors outside the defined status palette. Custom label colors must go through the color-mix() tint treatment.
Rely on color alone to convey state. StateBadge pairs a Unicode glyph with a tone.
Skip the shadow-control transition on buttons. The subtle shadow lift is the primary hover feedback in dark themes.
Add animations outside the enter keyframe without ensuring they respect prefers-reduced-motion.
Use brand green #22c55e anywhere outside the SlopMark SVG.
src/app/globals.css under the @theme block. This file is the canonical reference; DESIGN.md is the narrative explanation of why the values are what they are.