Slop Docs Home
Slop · Design

Design System

Carbon-inspired dark-only visual language. Token-based color, typography, spacing, and component rules for every UI surface in Slop.

Dark-only Carbon-inspired Token-based Tailwind CSS 4

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.

bg-base
#0f0f0f
bg-surface
#161616
bg-elevated
#1e1e1e
bg-overlay
#262626
TokenValueUsage
bg-base#0f0f0fTrue page floor, rarely painted directly
bg-surface#161616<body> background, Card background
bg-elevated#1e1e1eNav header, secondary button fill, surface-muted fills
bg-overlay#262626Dropdown panels, popovers, context menus

Text hierarchy

TokenValueUsage
text-primary#edededPrimary body copy, headings, active tab labels
text-secondary#737373Secondary/muted copy, inactive tab labels, subtitles
text-tertiary#404040Placeholder 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.

TokenValueUsage
border-subtle#222222Nav header bottom border, scrollbar track
border-default#2a2a2aCard 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.

accent
#8b5cf6
accent-hover
#7c3aed
accent-fg
#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.

Tonefg valuebg valueWhen to use
success#34d399#0a2816Merged, completed
info#67e8f9#0a1f2eIn-progress, CI waiting, has PR
warn#fcd34d#2e2000Needs attention, paused, conflict
danger#fc8181#3a0a0aFailed, PR closed, errors
neutral#737373#1e1e1eQueued, 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

StepValueLine heightUsage
text-xs13 pxdefaultBadges, button sm, footnotes
text-sm13 pxdefaultButton md, tab labels, secondary body copy
text-base15 pxdefaultPrimary body copy
text-lg17 pxdefaultSubheadings, card titles
text-xl21 pxdefaultLarge headings
text-page-title25 px2 remPage <h1> via PageHeader
text-section15 px1.25 remSection 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

ContextFontSizeWeight
Page <h1>sanstext-page-title (25 px)semibold (600)
Dialog titlesanstext-base (15 px)semibold (600)
Section heading labelsanstext-section (15 px)semibold (600), uppercase, tracking-wide
Card/subheadingsanstext-lg (17 px)semibold (600)
Body copysanstext-base (15 px)regular (400)
Secondary copysanstext-sm (13 px)regular (400)
Tab labelsanstext-sm (13 px)medium (500)
Badge labelsanstext-xs (13 px)medium (500)
Log/code outputmonotext-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.

TokenValueCommon use
spacing-14 pxIcon-to-label gap, tight inline gap
spacing-28 pxBadge padding, button vertical padding sm
spacing-312 pxButton horizontal padding sm, small gaps
spacing-416 pxStandard content padding, list item gap
spacing-520 pxModerate section spacing
spacing-624 pxCard padding, standard section spacing
spacing-832 pxPage horizontal padding
spacing-1040 pxGenerous 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.

TokenValueUsage
spacing.section2 remBetween major page sections (space-y-section)
spacing.block1.5 remPage title <header> to first content block (mt-block)
spacing.tight0.75 remSection 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

TokenValueUsage
shadow-control0 1px 2px rgb(0 0 0 / 0.35)Resting button, tooltip, menu panel
shadow-control-hover0 2px 8px rgb(0 0 0 / 0.45)Button on hover
shadow-node0 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

TokenValueUsage
rounded-sm3 pxVery tight corners: small inset labels
rounded-md6 pxDropdown items, popover inner rows
rounded-lg8 pxCards, dialog, popovers, tooltips
rounded-control8 pxAll buttons (primary, secondary, danger, destructive)
rounded-full9999 pxBadges, 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.

VariantBackgroundTextHover
primary#8b5cf6 (accent)white#7c3aed fill
secondary#1e1e1e (bg-elevated)text-primaryopacity 90%
danger#161616 (bg-surface)#fc8181 (danger-fg)danger-bg fill
ghost-dangertransparent#fc8181 (danger-fg)opacity 70%
destructive#7f1d1d#fca5a5opacity 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}".

ToneStates
neutralqueued, claimed, cancelled, closed
infoimplementing, verifying, waiting_ci, waiting_review, in_review, waiting_merge, merging, reporting, has_pr
warnwaiting_address, in_address, paused, resolving_conflict, fixing_ci, refining, reviewing
successmerged, reviewed, refined
dangerfailed, 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

Do

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.

Don't

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.

Single source of truth: All color, spacing, and type tokens live in 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.