Roman's Pizza Design System
Design tokens, Lucide icons, and component classes — one source of truth for all RP digital products.
Gradients
All gradients follow a single rule: lightest colour at the top, darkest at the bottom. This creates natural visual hierarchy — the eye tracks downward through increasing weight — and anchors heavier UI elements (tables, footers, chart axes) with a darker, more grounded base.
Gradient tokens
--g-blue
Buttons, hero cards, badges, CTAs
--g-sidebar
Sidebar nav, dark panels
Surface gradient
Inline use — subtle depth on cards
Direction rule
Correct — light top, dark bottom
Incorrect — dark top, light bottom
Usage in CSS
/* Use the token — never repeat the raw gradient */
.my-hero { background: var(--gradient-brand-blue); }
.my-panel { background: var(--gradient-sidebar); }
/* Custom one-off: always light → dark, top → bottom */
.my-banner {
background: linear-gradient(180deg, #0260c9 0%, #014085 100%);
}Colours
Brand
#014085
#012d5e
#0260c9
#ed1c24
#c41820
Brand Ink & Action
Brand blue per theme. Ink for links, focus rings and the active tab; action for the fill behind white text. Light and dark values shown.
#014085 / #8fb8f0
#014085 / #0260c9
#012d5e / #014085
Overlay & Selection Surfaces
Opaque surface for menus, popovers and modals: Radix Themes overrides --color-surface with a translucent value inside its root.
#fff / #0f1117
open or selected row
Semantic Status
Rating Scale
Ember at 1 star, grey at 3, brand blue at 5. Readable with red-green colour blindness. Print the number beside the colour.
#9a3b0c / #f0955c
#e0915a / #b0602f
#cbcfd6 / #3d424f
#6a8ec4 / #4f7cc2
#014085 / #8fb8f0
Typography
All typography values are defined as CSS custom properties in src/tokens/typography.css. Components reference these tokens — never use raw pixel or rem values.
Font Size Scale
Font Weights
| Token | Value | Preview |
|---|---|---|
| --font-weight-regular | 400 | Regular weight text |
| --font-weight-medium | 500 | Medium weight text |
| --font-weight-semibold | 600 | Semibold weight text |
| --font-weight-bold | 700 | Bold weight text |
| --font-weight-extrabold | 800 | Extrabold weight text |
Line Heights & Letter Spacing
| Token | Value | Use |
|---|---|---|
| --line-height-tight | 1.15 | Stat values, large headings |
| --line-height-snug | 1.2 | Buttons, compact text |
| --line-height-normal | 1.4 | General content |
| --line-height-base | 1.5 | Body text default |
| --line-height-relaxed | 1.6 | Long-form text, descriptions |
| --letter-spacing-tight | -0.02em | Large headings |
| --letter-spacing-normal | 0.01em | Body text |
| --letter-spacing-wide | 0.04em | Buttons, badges |
| --letter-spacing-wider | 0.07em | Labels, uppercase text |
| --letter-spacing-widest | 0.075em | All-caps headings |
Spacing & Radii
Spacing Scale
Semantic Spacing
| Token | Value | Use |
|---|---|---|
| --spacing-page | 2rem | Outer page padding |
| --spacing-section | 2rem | Gap between major sections |
| --spacing-card | 1.375rem | Internal card padding |
Radius Tokens
Shadows
Shadow Tokens
| Token | Light Mode | Use |
|---|---|---|
| --shadow-xs | 0 1px 2px 5% black | Subtle depth, swatches |
| --shadow-sm | Dual layer, 6%+4% | Default elevation |
| --shadow-md | Dual layer, 5%+4% | Raised elements |
| --shadow-lg | Dual layer, 5%+3% | Prominent elements |
| --shadow-card | Dual layer, 7%+5% | Card default |
| --shadow-card-hover | Dual layer, blue-tinted | Card hover state |
| --shadow-pop | Deep dual layer | Modals, overlays |
| --shadow-glow | Blue brand glow | Focus rings, emphasis |
Motion
| Token | Value |
|---|---|
| --transition-fast | 150ms ease |
| --transition-base | 250ms ease |
| --ease-settle | cubic-bezier(0.25, 0.46, 0.45, 0.94) |
| --ease-spring | cubic-bezier(0.175, 0.885, 0.32, 1.275) |
Breakpoints
The design system uses three breakpoints. Desktop styles are the default — overrides are applied at narrower viewports via max-width queries. This ensures existing desktop appearance is never affected.
Breakpoint Architecture
| Name | Query | Range | Min Supported |
|---|---|---|---|
| Mobile | max-width: 767px | 0–767px | 375px |
| Tablet | min-width: 768px AND max-width: 1023px | 768–1023px | 768px |
| Desktop | min-width: 1024px | 1024px+ | 1024px |
Combined Queries
| Alias | Query | Covers |
|---|---|---|
| ≤ Tablet | max-width: 1023px | Mobile + Tablet |
| ≥ Tablet | min-width: 768px | Tablet + Desktop |
Usage in CSS
/* Mobile only */
@media (width <= 767px) {
.my-component { padding: 1rem; }
}
/* Tablet only */
@media (768px <= width <= 1023px) {
.my-component { padding: 1.5rem; }
}
/* Mobile + Tablet (≤ 1023px) */
@media (width <= 1023px) {
.my-grid { grid-template-columns: 1fr; }
}Buttons
<button class="rp-btn rp-btn--primary">Call</button> <button class="rp-btn rp-btn--secondary">Store PDF</button>
Badges
Cards
Stat Grid
Total Reviews
1,248
+12% vs last month
Avg Rating
4.3
out of 5
Promoters
68%
Semantic Tints
Success
Warning
Error
Info
Accent Left-Border
Success
Warning
Error
Chips
Tabs & Counts
Page tabs on a hairline. The selected tab is set by aria-selected; many tabs scroll sideways at phone width.
<div class="rp-tabs" role="tablist">
<button class="rp-tab" role="tab" aria-selected="true">Overview</button>
<button class="rp-tab" role="tab" aria-selected="false">
Triage <span class="rp-count">12</span>
</button>
</div>Count pill
Segmented Control
A few options shown together, one chosen. The pressed option is set by aria-pressed.
<div class="rp-segmented" role="group" aria-label="Period"> <button type="button" aria-pressed="false">7 days</button> <button type="button" aria-pressed="true">30 days</button> <button type="button" aria-pressed="false">90 days</button> </div>
Popover Menus
A picker or menu under its trigger, on the opaque surface. Opening, closing and keyboard movement belong to the app.
<div class="rp-popover-anchor">
<button class="rp-btn rp-btn--secondary" aria-expanded="true">All stores</button>
<div class="rp-popover" role="listbox">
<label class="rp-popover__search"><input placeholder="Find a store"></label>
<div class="rp-popover__list">
<button class="rp-popover__item" role="option" aria-selected="true">
All stores <span class="rp-popover__hint">1,248</span>
</button>
</div>
</div>
</div>Legends
Every colour, shape and line on a chart gets a key. Set any series colour with --color-legend-mark.
- 1 star
- 2 stars
- 3 stars
- 4 stars
- 5 stars
- Satisfied (4 or 5 stars)
- Estate
- Bad review (1 or 2 stars)
<ul class="rp-legend" aria-label="Star colours">
<li class="rp-legend__item">
<i class="rp-legend__swatch rp-legend__swatch--rating-5" aria-hidden="true"></i>5 stars
</li>
<li class="rp-legend__item">
<i class="rp-legend__swatch rp-legend__swatch--line"
style="--color-legend-mark: var(--color-on-surface-muted)" aria-hidden="true"></i>Estate
</li>
</ul>Bar Charts
- 1 star
- 2 stars
- 3 stars
- 4 stars
- 5 stars
Charts
Charts are inline SVG painted with the .rp-plot classes, as the CMS draws them: house type, tokens and dark mode, no charting library. Colour carries the rating and nothing else, and every mark has a key. Sample data.
Satisfied, week by week
- 4-week average
- 95% range
- One week, sized by reviews
Reviews by star, week by week
- 1 star
- 2 stars
- 3 stars
- 4 stars
- 5 stars
Stores against the group
- Group
- 95% limit
- 99.8% limit
- Above
- Below
Stores, last 6 weeks
| Store | Satisfied | Satisfied trend | Complaints |
|---|---|---|---|
| Mossel Bay | 92% | ||
| Midrand | 80% | ||
| Germiston CBD | 62% |
Satisfied, last 4 weeks
79%
Up 4 points on the 4 weeks before
| Till slip | 81% 86 reviews | |
|---|---|---|
| Web form | 79% 61 reviews | |
| Online order | 74% 27 reviews |
Down 6 points at Germiston CBD Reviews up 18%
Tags, complaints and praise
- On a complaint
- On praise
| 14 | Slow | 2 |
|---|---|---|
| 9 | Cold | 1 |
| 2 | Friendly | 18 |
| 1 | Hot | 15 |
| 6 | Wrong order | 0 |
Plot classes
<div class="rp-chart-card">
<div class="rp-chart-header">
<h3 class="rp-chart-title">Satisfied, week by week</h3>
<ul class="rp-legend">…</ul>
</div>
<div class="rp-plot-wrap">
<svg class="rp-plot" viewBox="0 0 720 300" role="img" aria-label="…">
<line class="rp-plot__grid" …/> <text class="rp-plot__tick rp-plot__end">80%</text>
<polygon class="rp-plot__interval" …/> <path class="rp-plot__line" …/>
<circle class="rp-plot__point" …/> <rect class="rp-plot__vol" …/>
<rect class="rp-plot__fill--rating-5" …/> <circle class="rp-plot__dot rp-plot__dot--below" …/>
<text class="rp-plot__tick rp-plot__halo">label over data</text>
</svg>
<div class="rp-plot-tooltip">…</div>
</div>
</div>
<div class="rp-spread"><span class="rp-spread__seg--rating-1" style="width:8%"></span>…</div>
<span class="rp-delta"><svg class="rp-delta__tri rp-delta__tri--good" viewBox="0 0 10 10"><path d="M1 8h8L5 1z"/></svg>Up 4 points</span>
<svg class="rp-spark rp-spark--complaint" …>…</svg>
<table class="rp-diverging">…</table>Using Chart.js instead? The card classes and docs/chart-integration.md still apply; its palette now follows the same rule (ink, muted grey, the rating scale).
Tables
| Branch | Reviews | Avg Rating | Status |
|---|---|---|---|
| Sandton City | 312 | 4.6 | Active |
| Rosebank | 198 | 4.1 | Pending |
| Midrand | 87 | — | Archived |
Inputs
Modals
Rate Your Order
This is the modal card component. Backdrop fade and slide-up animate on mount.
Empty States
No reviews yet
Once customers leave reviews they'll appear here. Share your link to get started.
Loading States
Utilities
Utility classes from src/components/utilities.css and src/components/responsive.css. These work as single-purpose helpers on any element.
Text Truncation — .rp-truncate
This is a very long text string that will be truncated with an ellipsis because the container is only 250px wide.
Container constrained to 250px — text truncated with ellipsis.
Tabular Numbers — .rp-tabular-nums
Without
1,111
8,888
4,204
With .rp-tabular-nums
1,111
8,888
4,204
Tabular nums align digits in columns — critical for stat cards, tables, and numeric lists.
Horizontal Scroll — .rp-scroll-x
Horizontally scrollable container with touch momentum and thin scrollbar.
Responsive Visibility
| Class | Mobile | Tablet+ |
|---|---|---|
| .rp-hide-mobile | Hidden | Visible |
| .rp-show-mobile | Visible | Hidden |
| .rp-stack-mobile | Stacks flex children vertically | Normal flex direction |
Screen Reader Only — .sr-only
The .sr-only class visually hides an element while keeping it accessible to screen readers. Use for skip links, form labels, or additional context.
Icons
Why Lucide SVG icons exclusively
aria-label or are hidden with aria-hidden="true" when decorative, giving full control over the announcement.
currentColor, meaning icons respond to semantic colour tokens (--color-success, --color-error, etc.) and work correctly in dark mode without any extra markup.
width/height attributes map directly to the design grid. Emoji size is controlled by font-size and behaves inconsistently inside flex and grid layouts.
Click any icon to copy its usage snippet. Install: npm i lucide or see How to Use.
Loading icons…
Dark Mode
Add data-theme="dark" or the class dark to <html> or any ancestor. Every token switches; no extra stylesheet. The class is what Radix Themes, Tailwind and next-themes set.
Theme Toggle Component
<!-- Theme toggle component --> <button class="rp-theme-toggle" aria-label="Toggle dark mode"> <svg class="rp-theme-toggle__icon--light" data-lucide="moon" width="16" height="16"></svg> <svg class="rp-theme-toggle__icon--dark" data-lucide="sun" width="16" height="16"></svg> </button>
Implementation
/* 1. Include theme-init.js in <head> (prevents FOUWT) */
<script src="theme-init.js"></script>
/* 2. Wire the toggle button */
const btn = document.querySelector('.rp-theme-toggle');
btn.addEventListener('click', () => {
const next = document.documentElement.dataset.theme === 'dark' ? 'light' : 'dark';
document.documentElement.dataset.theme = next;
localStorage.setItem('rp-theme', next);
});
/* 3. CSS fallback (no JS): prefers-color-scheme in theme-fallback.css */
/* OS theme changes are auto-detected via matchMedia listener */
/* Class-based apps (Radix Themes, Tailwind, next-themes): .dark works too.
Mark light mode on the root as well (class="light" or data-theme="light"),
or the OS-preference fallback turns the tokens dark on a dark OS. */
<div class="dark">…</div>Light vs Dark preview
Light (default)
Total Reviews
1,248
Dark
Total Reviews
1,248
Token changes in dark mode
| Token | Light | Dark | Role |
|---|---|---|---|
| --color-background | #f3f4f6 | #0a0c14 | Page background, one step below the surface |
| --color-surface | #fff | #0f1117 | Card / panel background |
| --color-surface-opaque | #fff | #0f1117 | Menus, popovers, modals |
| --color-surface-dim | #f8f9fa | #141720 | Subtle surface / table header |
| --color-surface-container | #f3f4f6 | #1a1d2b | Secondary control background |
| --color-surface-selected | rgb(1 64 133 / 6%) | rgb(143 184 240 / 8%) | Open or selected row |
| --color-on-surface | #111827 | #f1f5f9 | Primary text |
| --color-on-surface-muted | #6b7280 | #94a3b8 | Secondary / placeholder text |
| --color-outline | #d1d5db | #3f4f6a | Input borders |
| --color-outline-variant | #e5e7eb | #2a3348 | Subtle borders, cards |
| --color-brand-ink | #014085 | #8fb8f0 | Links, focus rings, active tab |
| --color-brand-action | #014085 | #0260c9 | Primary button fill |
| --color-rating-1 to -5 | See Colours, Rating Scale | Star ratings | |
Icon colour utilities
Apply these classes directly to any Lucide <svg>. Use the parent .icon-hover--* classes to change icon colour on hover.
<!-- Colour class on the svg --> <svg data-lucide="check-circle" width="20" height="20" class="icon--ok"></svg> <svg data-lucide="alert-triangle" width="20" height="20" class="icon--warn"></svg> <!-- Icon button variants --> <button class="icon-btn"><svg data-lucide="copy" width="16" height="16"></svg></button> <button class="icon-btn icon-btn--blue"><svg data-lucide="download" width="16" height="16"></svg></button> <button class="icon-btn icon-btn--danger"><svg data-lucide="trash-2" width="16" height="16"></svg></button> <!-- Hover: change icon colour when parent is hovered --> <a class="icon-hover--blue" href="/"> <svg data-lucide="home" width="18" height="18"></svg> Home </a>
Responsive Preview
Live preview of components at each breakpoint. Each frame loads a lightweight component sampler constrained to its breakpoint width.
How to Use
CDN
<link rel="stylesheet" href="https://design.romanspizza.co.za/rp-style.css"> <script src="lucide.min.js"></script> <script>lucide.createIcons();</script>
npm
npm install lucide rp-design-system
// In your JS entry:
import { createIcons, icons } from 'lucide';
import 'rp-design-system/dist/rp-style.css';
createIcons({ icons });Icon usage
<!-- Add a data-lucide attribute, then call createIcons() once --> <svg data-lucide="star" width="20" height="20"></svg> <button class="rp-btn rp-btn--primary"> <svg data-lucide="download" width="14" height="14"></svg> Download </button>