Foundation
Colors
Semantic surface tokens layered with opacity-based dark-mode variants. No separate dark-mode named swatches — use dark: modifiers throughout.
Surfaces
white
Light base
gray-50
Subtle tint
gray-100
Hover / well
gray-900
Dark card
gray-950
Dark base
Brand
#800000
Primary CTA
#6a0000
Hover
#800000/20
Tint bg
red-900
Sidebar / nav
red-700
Alt primary
Status Accents
emerald-500
Approved
green-600
Attending
amber-500
Pending / warn
red-600
Danger
blue-500
Info
violet-500
Training
Dark Mode Border Tokens
border-white/10
Primary — cards, modals, nav
border-white/8
Subtle — outer card borders
border-white/15
Stronger — button outlines
border-white/5
Faintest — inner dividers
Foundation
Typography
Two families: Barlow Condensed (font-display) for headings and stats; Inter (default) for all UI text. Mono for code and data values.
Barlow Condensed — font-display
Wagenge FC — Match Day
Saturday 5 July · League
Card Heading / Section Title
Basket Name / Subheading
Inter — font-sans (default)
Subheading — 16px semibold
Body strong — 14px medium
Body default — 14px regular
Supporting / meta — 12px
SECTION LABEL — 10px bold uppercase tracked
MICRO LABEL — 9px sidebar group headers
Mono — font-mono
bg-gray-950 · border-white/10 · rounded-2xl · #800000
GET /design · 23:55 · KSH 2,500.00
<!-- Display heading -->
<h2 class="font-display text-2xl font-bold text-gray-900 dark:text-white">...</h2>
<!-- Section label (all-caps, tracked) -->
<p class="text-[10px] font-bold uppercase tracking-[0.15em] text-gray-400 dark:text-gray-500">...</p>
<!-- Body -->
<p class="text-sm text-gray-600 dark:text-gray-300">...</p>
<!-- Muted / meta -->
<p class="text-xs text-gray-500 dark:text-gray-400">...</p>
Foundation
Spacing & Radii
Consistent padding and corner-radius tokens used throughout. Pick the right radius for the element's role, not its size.
Corner Radii
rounded-lg (8px)
Small buttons, tags
rounded-xl (12px)
Buttons, inputs, month cells
rounded-2xl (16px)
Cards, modals, toasts
rounded-full (9999px)
Badges, avatars, dots
Padding Tokens
px-2 py-0.5
Micro — tight badges
px-3 py-1.5
Small — compact buttons
px-4 py-2
Medium — secondary actions
px-5 py-2.5
Default — primary buttons
px-4 py-3 / px-5 py-4
Card body — inner content
px-5 pt-5 pb-4
Card header — with bottom border
p-5 / p-6
Card padded — all sides
Components
Badges & Pills
Muted opacity tints — never solid fills. Status badges use ring-1 ring-inset for a clean boundary. Category pills use no ring and a square rounded shape.
Status Badges
Match Category Pills
Role & Eligibility
Count & Notification Badges
<!-- Status badge (rounded-full, ring-inset) -->
<span class="inline-flex items-center rounded-full px-2.5 py-1
text-[10px] font-bold uppercase tracking-[0.12em]
ring-1 ring-inset
bg-emerald-500/10 text-emerald-700 ring-emerald-500/25
dark:text-emerald-400">Approved</span>
<!-- Category pill (rounded, no ring) -->
<span class="block truncate rounded px-1.5 py-px
text-[11px] font-semibold leading-tight
bg-amber-400/25 text-amber-800 dark:text-amber-300">League</span>
<!-- Notification dot badge -->
<span class="absolute -right-1 -top-1 flex h-4 w-4 items-center justify-center
rounded-full bg-red-500 text-[9px] font-bold text-white
ring-2 ring-white dark:ring-gray-950">3</span>
Components
Form Elements
All fields use rounded-xl. Focus ring: focus:ring-2 focus:ring-red-500/20. Labels always use the 10px uppercase tracked pattern.
Light Surface
Dark Surface (modal)
Form Group — Validation States
Helper text goes here
Looks good!
Enter a valid email address
Checkboxes
<!-- Standard input -->
<label class="mb-1.5 block text-[10px] font-bold uppercase tracking-[0.12em] text-gray-500 dark:text-gray-400">Label</label>
<input type="text"
class="w-full rounded-xl border border-gray-200 bg-white px-3.5 py-2.5 text-sm
text-gray-900 shadow-sm transition
focus:border-red-500/60 focus:outline-none focus:ring-2 focus:ring-red-500/20
dark:border-white/10 dark:bg-white/[0.06] dark:text-white">
<!-- Error state -->
<input class="... border-red-400 ring-2 ring-red-400/20 dark:border-red-500/60">
<p class="mt-1.5 text-xs text-red-600 dark:text-red-400">Error message</p>
Components
Avatars
Use the user_avatar(user, size:) helper. For players, user_avatar_with_eligibility_badge adds a coloured status dot. Default background is bg-red-900.
Sizes
xs
sm
md
lg
with img
Eligibility Badge (player only)
Eligible
Eligible (manual)
Partial
Ineligible
Badge colors: bg-emerald-500 eligible · bg-blue-500 manual exception · bg-yellow-500 partial · bg-red-500 ineligible
User Row (name + role combo)
Brian Kimani
Player
Joyce Wanjiru
Admin
Tom Odhiambo
Fan
<!-- ERB helpers (recommended) -->
<%= user_avatar(@user, size: "md") %>
<%= user_avatar(@user, size: "lg") %>
<%= user_avatar_with_eligibility_badge(@user, size: "md") %>
<!-- size: "xs" | "sm" | "md" | "lg" -->
<!-- Raw HTML — initials avatar -->
<div class="flex h-10 w-10 shrink-0 items-center justify-center
rounded-full bg-red-900 text-sm font-bold text-white shadow-md">
BK
</div>
<!-- Sizes: xs=h-6·w-6·text-xs sm=h-8·w-8·text-sm md=h-10·w-10·text-sm lg=h-16·w-16·text-xl -->
<!-- Raw HTML — avatar + eligibility badge -->
<div class="relative shrink-0">
<div class="flex h-10 w-10 items-center justify-center
rounded-full bg-red-900 text-sm font-bold text-white shadow-md">BK</div>
<span class="absolute -bottom-0.5 -right-0.5 h-4 w-4 rounded-full
ring-2 ring-white dark:ring-gray-900 bg-emerald-500"></span>
</div>
<!-- Badge sizes: xs=h-2.5·w-2.5 sm=h-3.5·w-3.5 md=h-4·w-4 lg=h-5·w-5 -->
<!-- Badge colors: bg-emerald-500 eligible · bg-blue-500 manual exception
bg-yellow-500 partial · bg-red-500 ineligible -->
<!-- Raw HTML — user row (avatar + name + meta) -->
<div class="flex items-center gap-3">
<div class="relative shrink-0">
<div class="flex h-9 w-9 items-center justify-center
rounded-full bg-red-900 text-sm font-bold text-white shadow-sm">BK</div>
<!-- badge here if player -->
</div>
<div>
<p class="text-sm font-semibold text-gray-900 dark:text-white">Brian Kimani</p>
<p class="text-xs text-gray-500 dark:text-gray-400">Player</p>
</div>
</div>
Layout
Cards
Always rounded-2xl. Two surface depths: card (gray-900) and deep/modal (gray-950). Inner dividers use thinner tokens than the outer border.
Standard card
Match
Attendance
Submit your availability for Saturday's match against Kahawa FC.
Modal / deep card
Admin
Start Attendance Voting
Card body content goes here.
Profile hero card
Brian Kimani
#7"Kimo"
Admin / accent-bordered card
Admin Only
Contribution Exemptions
Mark basket months as exempt from contributions.
Card body…
<!-- Standard card -->
<div class="overflow-hidden rounded-2xl border border-gray-100 bg-white
dark:border-white/10 dark:bg-gray-900">
<div class="border-b border-gray-100 px-5 py-4 dark:border-white/5">header</div>
<div class="px-5 py-4">body</div>
</div>
<!-- Modal / deep card -->
<div class="overflow-hidden rounded-2xl border border-white/10 bg-gray-950 shadow-2xl">
<div class="border-b border-white/5 px-5 py-4">header</div>
<div class="px-5 py-4">body</div>
<div class="border-t border-white/5 flex justify-end gap-2 px-5 py-4">footer</div>
</div>
<!-- Profile hero (crimson strip) -->
<div class="overflow-hidden rounded-2xl border border-gray-200 bg-white
dark:border-gray-800 dark:bg-gray-950">
<div class="h-1 bg-gradient-to-r from-[#800000] via-[#800000]/50 to-transparent"></div>
...
</div>
Layout
Lists
Two patterns: divide-y for compact data rows, space-y-3 card grid for rich items. Hover uses hover:bg-gray-50 / dark:hover:bg-gray-900.
Divide-y List (members, players)
Brian Kimani
Player
Joyce Wanjiru
Admin
Tom Odhiambo
Fan
Card List (matches)
Wagenge FC vs Kahawa FC
League · Sat 5 Jul
Wagenge FC vs Park FC
Friendlies · Sun 6 Jul
Wagenge FC vs Garden FC
Tournament · Mon 7 Jul
<!-- Divide-y list wrapper -->
<div class="overflow-hidden rounded-2xl border border-gray-100 bg-white
dark:border-white/10 dark:bg-gray-900">
<!-- Row (add border-t on all but first) -->
<div class="flex items-center gap-3 px-5 py-3.5 transition-colors
hover:bg-gray-50 dark:hover:bg-white/[0.03]
border-t border-gray-100 dark:border-white/5">...</div>
</div>
<!-- Card list item -->
<div class="flex items-center gap-4 rounded-2xl border border-gray-100 bg-white
px-5 py-4 transition-all
hover:border-gray-200 hover:shadow-sm
dark:border-white/10 dark:bg-gray-900">...</div>
Feedback
Toasts & Flash
Auto-dismiss after 5s via auto-dismiss Stimulus controller. Fixed overlay container: #flash-messages. Rendered by shared/_flash_message.
Success
This is a sample success toast message.
Error
This is a sample error toast message.
Warning
This is a sample warning toast message.
Info
This is a sample info toast message.
Inline Flash (inside panels / modals — no shadow, no dismiss)
That date has already passed. Please select a future date.
Changes saved successfully.
You have unsubmitted changes.
<!-- Turbo Stream toast -->
<%= turbo_stream.append "flash-messages" do %>
<%= render "shared/flash_message", type: :notice, message: "Saved!" %>
<% end %>
<!-- Types: :notice | :alert | :warning | :info -->
<!-- Inline flash (error) -->
<div class="flex items-start gap-2 rounded-xl
border border-red-500/20 bg-red-500/10 px-3 py-2.5">
<!-- icon -->
<p class="text-sm text-red-700 dark:text-red-300">Message</p>
</div>
Feedback
Empty States
Used when a list or section has no data. Always center-aligned with a descriptive icon and an optional CTA.
Card empty state
No matches yet
When a match is scheduled it will appear here.
Dashed border (permissions / settings)
No permission
You don't have access to manage this section.
Inline empty (inside a list)
Recent transactions
No transactions recorded.
Contextual color (eligible players)
No eligible players
All players are currently ineligible.
<!-- Card empty state (standard) -->
<div class="rounded-2xl border border-gray-100 bg-white px-6 py-12 text-center
dark:border-white/10 dark:bg-gray-900">
<div class="mx-auto mb-4 flex h-12 w-12 items-center justify-center
rounded-full bg-gray-100 dark:bg-gray-800">
<!-- svg icon in text-gray-400 dark:text-gray-500 -->
</div>
<p class="text-sm font-semibold text-gray-700 dark:text-gray-300">No matches yet</p>
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">Descriptive helper text.</p>
<!-- optional CTA button -->
</div>
<!-- Dashed border empty state (permissions / settings sections) -->
<div class="rounded-2xl border border-dashed border-gray-200 py-12 text-center
dark:border-gray-700">
<!-- same inner structure -->
</div>
<!-- Inline empty (inside an existing card list) -->
<div class="px-5 py-10 text-center">
<p class="text-sm text-gray-400 dark:text-gray-500">No transactions recorded.</p>
</div>
<!-- Contextual tinted empty state (colour matches the data type) -->
<div class="rounded-2xl border border-emerald-100 bg-emerald-50/60 px-6 py-10 text-center
dark:border-emerald-500/20 dark:bg-emerald-500/[0.06]">
<div class="mx-auto mb-3 flex h-11 w-11 items-center justify-center
rounded-full bg-emerald-100 dark:bg-emerald-900/40">
<!-- svg icon in text-emerald-600 dark:text-emerald-400 -->
</div>
<p class="text-sm font-semibold text-emerald-700 dark:text-emerald-300">Label</p>
<p class="mt-1 text-xs text-emerald-600/70 dark:text-emerald-400/60">Description</p>
</div>
<!-- Swap emerald → amber / red / blue to match the section context -->
Patterns
Stats & Metrics
Numbers use font-display font-bold for visual weight. Group related stats in a 3-column strip.
3-Column Stat Strip (attendance)
78%
Attendance
14
Attended
18
Total matches
Payment Status Summary
8
Paid
months
2
Partial
months
2
Unpaid
months
Year Summary Card (brand-tinted)
2026 Summary
Total contributed
KSH 28,500
Transactions
14
Active baskets
3
Dashboard Stat Buttons (attendance voting)
<!-- 3-column stat strip -->
<div class="grid grid-cols-3 gap-3">
<div class="rounded-2xl border border-gray-200 bg-white p-4 text-center
dark:border-gray-800 dark:bg-gray-950">
<p class="font-display text-2xl font-bold text-gray-900 dark:text-white">78%</p>
<p class="mt-0.5 text-xs text-gray-500 dark:text-white/45">Attendance</p>
</div>
</div>
<!-- Year summary card (brand-tinted) -->
<div class="rounded-2xl border border-[#800000]/20 bg-[#800000]/5 p-5
dark:border-[#800000]/30 dark:bg-[#800000]/15">
<p class="text-xs font-semibold uppercase tracking-[0.2em]
text-[#800000] dark:text-red-400/80">2026 Summary</p>
<div class="mt-4 grid grid-cols-3 gap-4">
<div>
<p class="text-xs text-[#800000]/60 dark:text-red-400/55">Label</p>
<p class="mt-0.5 font-display text-xl font-bold text-[#800000] dark:text-red-300">Value</p>
</div>
</div>
</div>
<!-- Dashboard stat button (attendance voting count) -->
<button class="rounded-xl border px-2 py-2.5 text-center transition-all hover:brightness-105
border-emerald-100 bg-emerald-50
dark:border-emerald-800/50 dark:bg-emerald-900/10">
<p class="font-display text-xl font-bold leading-none text-gray-900 dark:text-white">12</p>
<p class="mt-1 text-[9px] font-bold uppercase tracking-[0.12em]
text-emerald-600 dark:text-emerald-400">Attending</p>
</button>
<!-- Colour variants for stat buttons -->
<!-- Attending : border-emerald-100 bg-emerald-50 dark:border-emerald-800/50 dark:bg-emerald-900/10 text-emerald-600 -->
<!-- Not attend : border-red-100 bg-red-50 dark:border-red-800/50 dark:bg-red-900/10 text-red-600 -->
<!-- Pending : border-amber-100 bg-amber-50 dark:border-amber-800/50 dark:bg-amber-900/10 text-amber-600 -->
<!-- Total : border-gray-100 bg-white dark:border-gray-700 dark:bg-gray-900 text-gray-500 -->
Patterns
Voting & Toggle UI
Attendance and role toggle buttons. Unselected state: muted dark surface. Selected state: full-opacity brand color with shadow-sm.
Unselected
Selected — attending + role
Role
<!-- Vote toggle button — unselected -->
<button class="flex flex-col items-center justify-center gap-1.5
rounded-xl border border-white/15 bg-white/[0.06] px-3 py-3
text-gray-300 transition-colors
hover:border-green-500/30 hover:bg-green-500/10">
<!-- icon -->
<span class="text-xs font-semibold">Attending</span>
</button>
<!-- Vote toggle button — selected (attending) -->
<button class="flex flex-col items-center justify-center gap-1.5
rounded-xl border border-green-500 bg-green-600 px-3 py-3
text-white shadow-sm">
<!-- icon -->
<span class="text-xs font-bold">Attending</span>
</button>
<!-- Vote toggle button — selected (not attending) -->
<button class="... border border-red-500 bg-red-600 text-white shadow-sm">
<!-- Role picker pill grid -->
<div class="rounded-xl border border-white/10 bg-white/5 px-4 py-3">
<p class="mb-2 text-[10px] font-bold uppercase tracking-[0.12em] text-gray-500">Role</p>
<div class="grid grid-cols-3 gap-1.5">
<!-- Active -->
<button class="rounded-lg border bg-[#800000] border-[#800000]
px-2 py-1.5 text-xs font-bold text-white">Striker</button>
<!-- Inactive -->
<button class="rounded-lg border border-white/10 bg-white/[0.04]
px-2 py-1.5 text-xs text-gray-400 hover:bg-white/10">Midfielder</button>
</div>
</div>
Patterns
Modals & Overlays
Bottom-sheet on mobile (rounded-t-2xl), centered dialog on sm+. Backdrop: bg-black/70. Panel surface: bg-gray-950.
Admin
Confirm action
This action will permanently remove the match and all associated attendance records. This cannot be undone.
<!-- Backdrop + panel (Stimulus modal controller) -->
<div class="fixed inset-0 z-50 flex items-end sm:items-center justify-center
bg-black/70 p-0 sm:p-4"
data-controller="modal">
<div class="bg-white dark:bg-gray-950
rounded-t-2xl sm:rounded-2xl
shadow-2xl border border-gray-200/60 dark:border-white/5
w-full sm:max-w-md">
<!-- Header -->
<div class="flex items-center justify-between
border-b border-gray-100 dark:border-white/5 px-5 py-4">
<h3 class="text-base font-bold text-gray-900 dark:text-white">Title</h3>
<button data-action="modal#close"
class="flex h-8 w-8 items-center justify-center rounded-xl
text-gray-400 hover:bg-gray-100 hover:text-gray-600
dark:hover:bg-white/10 dark:hover:text-white">✕</button>
</div>
<!-- Body -->
<div class="px-5 py-5">...</div>
<!-- Footer -->
<div class="flex justify-end gap-2
border-t border-gray-100 dark:border-white/5 px-5 py-4">
<button class="[secondary]">Cancel</button>
<button class="[primary]">Confirm</button>
</div>
</div>
</div>
Patterns
Dropdowns
Powered by the dropdown Stimulus controller. Menu panel: rounded-2xl shadow-2xl on gray-950. Destructive items turn red on hover.
Action menu
Role switcher (sidebar)
Switch role
<!-- Trigger -->
<div class="relative" data-controller="dropdown">
<button data-action="click->dropdown#toggle" data-dropdown-target="button">
Options
</button>
<!-- Menu -->
<div class="absolute right-0 top-full z-50 mt-1.5 w-48
overflow-hidden rounded-2xl border border-gray-200 bg-white
py-1 shadow-2xl
dark:border-white/10 dark:bg-gray-950"
data-dropdown-target="menu">
<!-- Item -->
<button class="flex w-full items-center gap-3 px-4 py-2.5 text-sm
text-gray-700 dark:text-gray-300
hover:bg-gray-50 dark:hover:bg-white/5">
Label
</button>
<!-- Divider -->
<div class="my-1 h-px bg-gray-100 dark:bg-white/5"></div>
<!-- Destructive item -->
<button class="flex w-full items-center gap-3 px-4 py-2.5 text-sm
text-red-600 dark:text-red-400
hover:bg-red-50 dark:hover:bg-red-500/10">
Delete
</button>
</div>
</div>
Patterns
Accordion
Powered by the accordion Stimulus controller. The chevron icon rotates 180° on open via data-accordion-target="icon".
Use the accordion controller to toggle this open. The chevron rotates 180° and content slides in. Works nested inside cards.
Content for squad selection section.
Content for financial summary section.
Standalone (inside a card, e.g. exemptions)
Accordion content goes here…
<div data-controller="accordion">
<button type="button"
class="flex w-full items-center justify-between gap-3 px-5 py-4
text-left transition-colors hover:bg-gray-50 dark:hover:bg-white/[0.03]"
data-action="click->accordion#toggle"
aria-expanded="false">
<span class="text-sm font-semibold text-gray-900 dark:text-white">
Heading
</span>
<svg class="h-4 w-4 text-gray-400 transition-transform"
data-accordion-target="icon" ...>
<path d="M19 9l-7 7-7-7"/>
</svg>
</button>
<div class="hidden px-5 py-4 border-t border-gray-100 dark:border-white/5"
data-accordion-target="content">
Content...
</div>
</div>
Wagenge FC Design System · GET /design · Public route · No auth required