Roman's Pizza Design System

Design tokens, Lucide icons, and component classes — one source of truth for all RP digital products.

v1.0 Lucide Icons Vanilla CSS Tailwind v4 ready Responsive Dark Mode

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

↑ #0260c9 light ↓ #014085 dark

--g-blue

Buttons, hero cards, badges, CTAs

↑ #014085 light ↓ #010e1f dark

--g-sidebar

Sidebar nav, dark panels

↑ #f8faff lightest ↓ #e8eef8 subtle

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

--color-brand-blue
#014085
--color-brand-blue-dark
#012d5e
--color-brand-blue-light
#0260c9
--color-brand-red
#ed1c24
--color-brand-red-dark
#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.

--color-brand-ink
#014085 / #8fb8f0
--color-brand-action
#014085 / #0260c9
--color-brand-action-hover
#012d5e / #014085

Overlay & Selection Surfaces

Opaque surface for menus, popovers and modals: Radix Themes overrides --color-surface with a translucent value inside its root.

--color-surface-opaque
#fff / #0f1117
--color-surface-selected
open or selected row

Semantic Status

success
success-light
warning
warning-light
error
error-light
info
info-light

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.

rating-1
#9a3b0c / #f0955c
rating-2
#e0915a / #b0602f
rating-3
#cbcfd6 / #3d424f
rating-4
#6a8ec4 / #4f7cc2
rating-5
#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-size-4xl · 2.5rem Empty State Icon
--font-size-3xl · 1.875rem Stat Card Value
--font-size-2xl · 1.375rem Page Header
--font-size-xl · 1.25rem Modal Title
--font-size-lg · 1rem Card Heading
--font-size-md-alt · 0.9375rem Button Large / Empty State
--font-size-md · 0.875rem Body text — Inter is the typeface across all RP products.
--font-size-base · 0.8125rem Button, Input, Card default
--font-size-sm-alt · 0.8rem Bar chart labels
--font-size-sm · 0.75rem Small / muted text
--font-size-xs · 0.6875rem LABEL CAPS
--font-size-2xs · 0.625rem Minimum text size (10px)

Font Weights

TokenValuePreview
--font-weight-regular400Regular weight text
--font-weight-medium500Medium weight text
--font-weight-semibold600Semibold weight text
--font-weight-bold700Bold weight text
--font-weight-extrabold800Extrabold weight text

Line Heights & Letter Spacing

TokenValueUse
--line-height-tight1.15Stat values, large headings
--line-height-snug1.2Buttons, compact text
--line-height-normal1.4General content
--line-height-base1.5Body text default
--line-height-relaxed1.6Long-form text, descriptions
--letter-spacing-tight-0.02emLarge headings
--letter-spacing-normal0.01emBody text
--letter-spacing-wide0.04emButtons, badges
--letter-spacing-wider0.07emLabels, uppercase text
--letter-spacing-widest0.075emAll-caps headings

Spacing & Radii

Spacing Scale

--spacing-1
0.25rem · 4px
--spacing-2
0.5rem · 8px
--spacing-3
0.75rem · 12px
--spacing-4
1rem · 16px
--spacing-5
1.25rem · 20px
--spacing-6
1.5rem · 24px
--spacing-8
2rem · 32px
--spacing-12
3rem · 48px
--spacing-16
4rem · 64px

Semantic Spacing

TokenValueUse
--spacing-page2remOuter page padding
--spacing-section2remGap between major sections
--spacing-card1.375remInternal card padding

Radius Tokens

xs 4
sm 6
md 8
lg 12
xl 16
2xl 24
full

Shadows

xs
sm
md
lg
card
hover
pop
glow

Shadow Tokens

TokenLight ModeUse
--shadow-xs0 1px 2px 5% blackSubtle depth, swatches
--shadow-smDual layer, 6%+4%Default elevation
--shadow-mdDual layer, 5%+4%Raised elements
--shadow-lgDual layer, 5%+3%Prominent elements
--shadow-cardDual layer, 7%+5%Card default
--shadow-card-hoverDual layer, blue-tintedCard hover state
--shadow-popDeep dual layerModals, overlays
--shadow-glowBlue brand glowFocus rings, emphasis

Motion

TokenValue
--transition-fast150ms ease
--transition-base250ms ease
--ease-settlecubic-bezier(0.25, 0.46, 0.45, 0.94)
--ease-springcubic-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

NameQueryRangeMin Supported
Mobilemax-width: 767px0–767px375px
Tabletmin-width: 768px AND max-width: 1023px768–1023px768px
Desktopmin-width: 1024px1024px+1024px

Combined Queries

AliasQueryCovers
≤ Tabletmax-width: 1023pxMobile + Tablet
≥ Tabletmin-width: 768pxTablet + 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

Active Pending Closed Draft Archived Brand
No dot variant

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

Positive 42 Negative 8 Neutral 17

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

4 12 1,248

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

5
90072%
4
18815%
3
756%
2
504%
1
353%
  • 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
40% 60% 80% 100% Reviews per week 56 0 3 Aug 17 Aug 31 Aug 14 Sep 28 Sep 12 Oct Selected period Promotion 70% guidance

Reviews by star, week by week

  • 1 star
  • 2 stars
  • 3 stars
  • 4 stars
  • 5 stars
50 25 25 50 0 3 Aug 17 Aug 31 Aug 14 Sep 28 Sep 12 Oct Selected period Promotion Good reviews Bad reviews

Stores against the group

  • Group
  • 95% limit
  • 99.8% limit
  • Above
  • Below
40% 60% 80% 100% Germiston CBD Mossel Bay 0 40 80 120 160 Reviews in the period

Stores, last 6 weeks

StoreSatisfiedSatisfied trendComplaints
Mossel Bay92%
Midrand80%
Germiston CBD62%

Satisfied, last 4 weeks

79%

Up 4 points on the 4 weeks before

Till slip81% 86 reviews
Web form79% 61 reviews
Online order74% 27 reviews

Down 6 points at Germiston CBD Reviews up 18%

Tags, complaints and praise

  • On a complaint
  • On praise
Reviews with each tag, as a complaint and as praise
14Slow2
9Cold1
2Friendly18
1Hot15
6Wrong order0

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

BranchReviewsAvg RatingStatus
Sandton City3124.6Active
Rosebank1984.1Pending
Midrand87—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

Excellent Great food Fast delivery Average Cold pizza OK value Would order again Best in area

Horizontally scrollable container with touch momentum and thin scrollbar.

Responsive Visibility

ClassMobileTablet+
.rp-hide-mobileHiddenVisible
.rp-show-mobileVisibleHidden
.rp-stack-mobileStacks flex children verticallyNormal 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

Screen reader clarity. Emojis are read aloud by screen readers with verbose, platform-dependent descriptions (e.g. “grinning face with big eyes”). Lucide SVGs accept an aria-label or are hidden with aria-hidden="true" when decorative, giving full control over the announcement.
Visual consistency across platforms. Emoji glyphs render differently on iOS, Android, Windows, and macOS — the same codepoint can produce wildly different shapes, colours, and sizes. SVG icons render pixel-identically everywhere.
Themeable with CSS. SVG strokes inherit currentColor, meaning icons respond to semantic colour tokens (--color-success, --color-error, etc.) and work correctly in dark mode without any extra markup.
Precise sizing and alignment. SVG width/height attributes map directly to the design grid. Emoji size is controlled by font-size and behaves inconsistently inside flex and grid layouts.
Brand safety. Emojis carry cultural and emotional connotations that vary by region and can undermine the professional tone of a business platform. A curated icon set keeps the interface neutral, functional, and on-brand.

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

Click to toggle — persists via localStorage
<!-- 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

Active Pending Issue

Dark

Total Reviews

1,248

Active Pending Issue

Token changes in dark mode

TokenLightDarkRole
--color-background#f3f4f6#0a0c14Page background, one step below the surface
--color-surface#fff#0f1117Card / panel background
--color-surface-opaque#fff#0f1117Menus, popovers, modals
--color-surface-dim#f8f9fa#141720Subtle surface / table header
--color-surface-container#f3f4f6#1a1d2bSecondary control background
--color-surface-selectedrgb(1 64 133 / 6%)rgb(143 184 240 / 8%)Open or selected row
--color-on-surface#111827#f1f5f9Primary text
--color-on-surface-muted#6b7280#94a3b8Secondary / placeholder text
--color-outline#d1d5db#3f4f6aInput borders
--color-outline-variant#e5e7eb#2a3348Subtle borders, cards
--color-brand-ink#014085#8fb8f0Links, focus rings, active tab
--color-brand-action#014085#0260c9Primary button fill
--color-rating-1 to -5See Colours, Rating ScaleStar ratings

Icon colour utilities

Apply these classes directly to any Lucide <svg>. Use the parent .icon-hover--* classes to change icon colour on hover.

.icon--blue
.icon--red
.icon--ok
.icon--warn
.icon--err
.icon--info
.icon--muted
.icon--white
<!-- 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.

Mobile 375px
Tablet 768px
Desktop ≥ 1024px

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>