Promise Design System

The language behind Promise — tokens, principles, and core components.

§ 01

Overview

Promise is an enterprise e-procurement product used by procurement officers, initiators, supervisors, and evaluation committees within a single tenant. This system exists to keep the product visually calm, information-dense, and trustworthy — and to let anyone invent new components that feel like they belong.

Think of this document as a grammar, not a catalog. Memorise the primitives (§ foundations), internalise the principles (§ below), and the components will compose themselves. When you need something new, return to the tokens.

§ 02

Principles

If a new component feels wrong, it usually violates one of these. Check here first.

01

Hairlines over shadows

Our surfaces separate by 1px hairlines. Shadows are reserved for true elevation (popovers, modals). Never stack shadows on inline surfaces.

02

Flat fills — never gradients

One color per region. Tints convey state (action-50, danger-50). Gradients add visual noise and fight the data.

03

Monochrome first, color for meaning

90% of the UI is ink on neutral surfaces. Color only appears when it communicates: action, status, warning. If it could be a chip, don't make it a button.

04

Typography earns its weight

13px is body. Headings jump, never creep. Use weight (500/600/700) not size for hierarchy within a block. Captions are 11–12px, uppercased.

05

Density is a feature

Procurement users scan thousands of rows. Pack information with rhythm (4px baseline, consistent control heights) so density doesn't become clutter.

06

Numbers are monospaced

IDR amounts, codes, IDs, timestamps — always in JetBrains Mono so columns of numbers align without tabular-nums gymnastics.

07

Role, not permission, drives the UI

The user sees the same app in a different shape based on active role. Gate actions by role; never show disabled UI where an entire view would be wrong.

08

One place for one thing

If two components answer the same question (e.g. Back button and breadcrumb), kill one. Duplication leaks into decision fatigue.

§ Foundation

Color

Two-tier system: primitives hold values, semantics express intent. Always consume the semantic token in components so dark mode flips automatically.

Primitives — Blue (action)
Primitives — Neutral (ink & surface)
Primitives — Status
Semantic tokens
color-surfaceBase card / panel backgroundneutral-0
color-surface-subtleInset regions, table headers, hoverneutral-25
color-page-bgApp canvas behind cardsneutral-50
color-hairline1px dividers between surfacesneutral-200
color-hairline-strongHover borders, stronger dividersneutral-300
color-ink-1Primary text — headings, data valuesneutral-900
color-ink-2Body text, default inkneutral-700
color-ink-3Secondary — descriptions, metaneutral-600
color-ink-4Tertiary — captions, table column headsneutral-500
color-action-500Primary buttons, links, active navblue-500
color-success-500Completed states, positive deltasgreen-500
color-warning-500Warning chips, pending badgesamber-500
color-danger-500Errors, destructive actionsred-500
Rules
Do
Use action-50 + action-100 borders for subtle info surfaces like banners.
Don't
Don't stack saturated colors (action-500 on success-500). Only one saturated hue per region.
Do
Read color from semantic tokens (color-ink-2), never primitives, inside components.
Don't
Don't hardcode hex in a component. If you need a value that isn't a token, the token is missing — add it.
§ Foundation

Typography

Inter for UI, JetBrains Mono for numbers and codes. Eight sizes cover every need — resist adding a ninth.

3xl · 28/700Page titleH1 only
2xl · 24/700Section headingH2
xl · 20/600SubsectionH3
lg · 16/500Emphasis bodystat values
md · 14/600Panel titleH4, titles
base · 13/400Body text — default ink on surfacesbody
sm · 12/500Meta, chips, field labelssecondary
xs · 11/600Caption · column headcaption
mono · 13PR-24038 · Rp46,620,000numbers, IDs
Rules
Do
Use weight (500 → 600 → 700) to establish hierarchy within a block before reaching for a bigger size.
Don't
Don't mix weights heavier than 700 or lighter than 400 — no black, no light, no italic except in metadata.
Do
Put every number (IDR, PR codes, IDs, timestamps) in monospace so columns align.
Don't
Don't create new sizes. If you need 18px, use lg (16) or xl (20) — the scale is intentionally sparse.
§ Foundation

Spacing

4px baseline grid. Named anchors for rhythm; never invent values in between.

space-2
4px · tight inline gap
space-3
6px · chip internal gap
space-4
8px · default inline gap
space-5
10px · row padding-y
space-6
12px · input padding-x
space-8
16px · card gutter
space-10
20px · panel padding
space-12
24px · section gap
space-16
32px · between sections
space-24
48px · page-header bottom
Control heights

Every interactive control snaps to one of these four heights. Combined with consistent padding, this is what creates the product's rhythm.

ctrl-sm
ctrl-md
ctrl-lg
ctrl-xl
§ Foundation

Radii

Smaller radii feel technical and trustworthy. We avoid fully rounded except on pills.

xs · 4
keyboard shortcuts
sm · 6
chips, small icons
md · 8
buttons, inputs
lg · 10
menus, popovers
xl · 12
panels, cards
pill
status chips
§ Foundation

Elevation

Sparse by design. Inline UI uses hairlines; shadows signal true layering (popovers, drawers, modals).

flat · hairline
shadow-1
shadow-2
shadow-3 · menu/modal
Rules
Do
Use shadow-3 on menus, popovers, and modal dialogs — things that float above the page.
Don't
Don't shadow cards, panels, or table rows. Use hairline borders to separate surfaces inline.
§ Foundation

Motion

Motion confirms cause-and-effect. Never decorates. Three durations, two easings.

dur-fastHover and focus states, icon color swaps.120ms
dur-baseMenu/popover open, panel expand, drawer slide.180ms
dur-slowPage transitions, success confirmations.260ms
ease-standardDefault for all functional motion.cubic-bezier(.2,.8,.2,1)
ease-emphEntrances and celebratory moments only.cubic-bezier(.16,1,.3,1)
§ Foundation

Iconography

Lucide — 1.5px stroke, 24×24 viewBox. Render at three sizes.

12 · xs
inside chips
14 · sm
buttons, inputs
16 · md
nav, topbar
20 · lg
page headers only
Rules
Do
Pair an icon with a label whenever possible. Standalone icons must have aria-label.
Don't
Don't mix icon libraries. Lucide only. If a glyph is missing, draw it in Lucide's style or omit it.
§ Component

Buttons

Four variants cover every case. If you need a fifth, compose from existing ones (e.g. btn-secondary + danger text).

Variants
Primary for the one important action per view. Secondary for everything else. Ghost for low-stakes commands in dense UIs.
<button class="btn btn-primary"><i data-lucide="plus"></i>Create package</button> // Tailwind <button class="inline-flex items-center gap-1.5 h-ctrl-md px-3.5 rounded-md bg-action-500 hover:bg-action-600 text-white text-sm font-medium"> <Plus class="w-3.5 h-3.5" />Create package </button>
Sizes
Default is md. Use sm in table rows and menu bars, lg only for top-of-form primary CTAs.
Rules
Do
One primary button per view, per decision. If there are two "primary" actions, one of them isn't primary.
Don't
Don't pair a primary with a secondary that does nearly the same thing. Pair primary with Cancel (ghost), never another filled button.
§ Component

Forms

36px control height. Labels above. Required is a subtle red dot, not a yelling asterisk.

Shown to vendors on the public announcement.
Value exceeds budget remaining (Rp42,000,000).
§ Component

Chips & status

Pill-shaped, 22px tall, with an optional leading status dot. Use chips for state — Draft, In Review, Approved. Never as a button.

Neutral Draft Approved Pending Rejected PKG-2026-0481 6 items
§ Component

Panels

The primary container. Panel = surface + 12px radius + hairline + optional head with title/desc + actions on the right.

panel.head panel.body slot: actions
Item Request 6 items
Items pulled from linked Purchase Requests.
Panel body — tables, forms, and detail content live here.
§ Component

Tables

Dense by default. Header is uppercase 11px ink-4 on surface-subtle. Rows separate by hairlines, hover on surface-subtle. Numbers right-aligned and mono.

PR NumberItemCategoryQtyTotal
PR-24021Feasibility Study — Wellsite AConsultancy1Rp12,000,000
PR-24021EIA Report — Wellsite AConsultancy1Rp8,000,000
PR-24038Site Drone SurveyTechnical3Rp4,500,000
§ Component

Stepper

Wizard with many steps → horizontal scroll. Circle is ring-only (white fill), check for done, ring-+-number for active, neutral for pending.

1
Step 1
Classify PR
2
Step 2
Initiate
3
Step 3
Register
4
Step 4
Announcement
5
Step 5
Aanwijzing
6
Step 6
Bid
§ Pattern

Role switcher

Promise is single-tenant, multi-role per user. The switcher lives at the top of the sidebar because role is context, not account.

Anatomy
Rules
Do
Show the user's role here; tenant name lives in the brand header above.
Don't
Don't repeat the active role anywhere else (topbar, breadcrumb). One source of truth.
§ Pattern

Detail layout

Two-column: content (1fr) + rail (320px). Title and meta at top, stepper under, then stacked panels. Rail carries summary cards (Readiness, Recent activity).

Title · chips · actions
Meta — author · timestamp · linked refs
Stepper
Panel — Item request
Panel — Procurement details
Readiness
Recent activity
§ Pattern

Density & rhythm

Vertical rhythm = 4px. Horizontal rhythm = 14–20px gutters. Every new component must snap to these.

  • Row padding-y: 10px (table rows, menu items, nav items)
  • Panel body padding: 20px; head padding: 14px 20px
  • Card gutter between panels: 16px
  • Section gutter on a page: 32px
  • Icon-to-label gap: 6px (buttons, chips), 10px (nav items)
  • Inline control gap: 8px (default), 10px (two buttons)
§ Reference

All tokens

Every design decision as a CSS variable. Source of truth: assets/tokens.css.

TokenValueIntent
§ Reference

Tailwind config

Drop into your app's tailwind.config.js as a preset. Semantic colors map to CSS variables, so dark mode flips via [data-theme="dark"].

tailwind.config.js
Import in your app
/* globals.css */ @import './tokens.css'; @tailwind base; @tailwind components; @tailwind utilities; /* Toggle dark mode by setting on <html> */ document.documentElement.setAttribute('data-theme', 'dark');