A calm, trustworthy system for hosts & their guests.
Wielo's design system is built for a real booking business — direct, professional, mobile-first. Modern and flat, one emerald palette pulled from the logo. This page is the canonical reference for every token, component and pattern. Copy the Tailwind config from the bottom and you're set.
Design principles
Trustworthy by default
Hosts share their Wielo URL with guests. Every screen must read as a real, professional business — not a hobby project.
Mobile is the default
Most hosts run their inbox from their phone. Design mobile-first, then progressively enhance for desktop.
Subtract, don't add
No gradients, no decoration, no shadow stacks. If a pixel doesn't earn its place, remove it.
One source of truth
Brand colour means brand colour. Status green means a confirmed booking. Never reuse a token for an unrelated job.
Logo
A stacked chevron forming a V, set in a rounded-22 square with the brand gradient. The gradient is the one place gradients are allowed — everything else stays flat.
Color
Wielo's palette is the emerald pulled directly from the logo. Bright emerald carries the brand and structure; deep emerald is reserved for emphasis — dark surfaces, featured badges, the second-stop of the brand gradient.
Brand palette
Brand gradient
The 135° linear gradient from #10B981 → #064E3B is the logo's primary surface. Use it on the logo itself, app icon backgrounds, and one hero element per screen — nowhere else.
Booking status
These five tones map 1:1 to booking states from the spec. Never use status colours outside their booking context.
Neutrals & surfaces
A slightly emerald-tinted neutral ramp — not pure grey — keeps everything sitting in the same family as the brand greens.
Typography
Two families. Plus Jakarta Sans for display + headings; Inter for everything else. Both load through next/font/google so there's no flash.
font-sans.Type scale
| Token | Size / weight | Sample |
|---|---|---|
| text-3xl · display | 30 / 700 | Welcome back, Lerato |
| text-2xl · display | 24 / 600 | Your bookings |
| text-xl · display | 20 / 600 | Listing performance |
| text-lg | 18 / 500 | Cape Town Boutique B&B |
| text-base | 16 / 400 | Three new booking requests are awaiting your confirmation. |
| text-sm | 14 / 400 | Check-in: Fri 12 Jul · 2 nights · 2 guests |
| text-xs | 12 / 500 | Booking reference |
| font-mono | JetBrains Mono · 14 | VILO-2026-AB1234 |
Spacing & layout
Stay on Tailwind's default 4-pt scale. No custom values. Always prefer flex/grid + gap over per-element margins.
Scale
Breakpoints
| Prefix | Width | Wielo context |
|---|---|---|
| — | 0 | Mobile · default · bottom nav |
| sm: | 640 | Large mobile |
| md: | 768 | Tablet · 2-col grids |
| lg: | 1024 | Desktop · sidebar appears |
| xl: | 1280 | Wide · max content 1280px |
Container
max-w-7xl mx-auto px-4 lg:px-8max-w-proseRadius & shadow
Two radii and two shadows do all the work. Hierarchy comes from surfaces and borders, not depth tricks.
Shadow scale
Motion
Three durations, one easing curve. Motion is functional — it signals state, it doesn't perform.
Hovers, focus rings, button colour changes.
Card lift, dropdown open, accordion toggle.
Drawers, sheets, modal enter / leave.
Use Tailwind's ease-out for entering elements, ease-in for leaving. Respect prefers-reduced-motion globally.
Iconography
lucide-react only. Stroke-based, 1.5px stroke at 16/20/24. One icon per concept — see the canonical mapping below.
Form inputs
All forms run through React Hook Form + Zod. Visuals based on shadcn/ui with the focus ring re-tinted to brand primary.
Avatars & user chips
Five sizes, three fallbacks (image, initials, icon). Always pillify with rounded-pill. Group/stack for shared listings or threads.
Search & filter chips
Directory front door. The search bar opens an autocomplete panel; selected filters render as a removable chip strip below the bar so guests always know what's narrowing the result set.
Tabs & segmented controls
Underline tabs for in-page section switching (e.g. listing editor). Pill segmented controls for binary or 2–3 view toggles. Vertical tabs in settings only.
Star rating
Always text-amber-400 for stars — universally readable. Three sizes match avatar sizes. Interactive form variant for review submission.
Date picker
The single most-used widget in the guest flow. Range selection with check-in / check-out, mid-range highlighting, blocked dates struck through, minimum-night warnings. Built on shadcn Calendar (react-day-picker under the hood).
Availability calendar
The host's calendar view. Larger cells with price overlays, drag-to-block selection, confirmed bookings rendered as filled spans with guest initials. iCal-synced bookings (Airbnb / Booking.com) get a subtle channel dot.
Dialogs & sheets
Modals for booking actions, sheets for filters and detail on mobile. Backdrop is bg-brand-dark/60 backdrop-blur-sm. Always one primary CTA; destructive actions confirm twice.
Cancel this booking?
Amara Okafor will be refunded R 1 200 (50% per your moderate policy) and notified by email. This can't be undone.
Booking request
Pending“Arriving on the 10:30 flight from Joburg. Could we drop bags before check-in?”
Filter listings
Showing 248 in Cape Town
Notification modals
One shell, six intents. Every notification dialog is the same max-w-sm card — identical padding, icon chip, type scale and footer. Only the icon, its tint and the buttons change. Backdrop is bg-brand-dark/60 backdrop-blur-sm. Always one primary CTA; destructive actions confirm twice. This is the canonical shell for every popup, alert, confirm and error in the app — in code, use <Modal> / the imperative modal.* helpers (and <FormModal> for popups that contain a form).
Booking confirmed
Amara Okafor's stay at Karoo Cottage is locked in. We've emailed her the check-in details and added it to your calendar.
Payout on the way
Your June payout of R 14 250 has been released to your bank. Paystack transfers usually clear within 1–2 business days.
Calendar not synced
We couldn't reach your Google Calendar for 3 days. New bookings may double-book until you reconnect.
Payment couldn't be processed
The guest's card was declined, so this booking wasn't charged. Nothing has been confirmed — you can ask them to retry.
Send this quote to Amara?
She'll get a notification and can accept within 24 hours. You can still edit the price until she pays.
Cancel this booking?
Amara Okafor will be refunded R 1 200 (50% per your moderate policy) and notified by email. This can't be undone.
<FormModal>)Add seasonal price
Karoo Cottage · overrides the base nightly rate
Status badges
Exact tone-pairs from the spec. Never invent a new colour for a new state — propose adding a row here instead.
Tables
Host booking list, admin user list, payments ledger — all share this base. Mobile rule: if it has more than 3 columns, render as cards on mobile (see Booking card pattern above).
| Reference | Guest | Dates | Status | Total | ||
|---|---|---|---|---|---|---|
| VILO-2026-AB1234 | AO Amara Okafor |
10–12 Jul · 2n | Pending | R 2 400 | ||
| VILO-2026-CD2891 | MK Marcus Khumalo |
15–18 Jul · 3n | Confirmed | R 3 600 | ||
| VILO-2026-EF4502 | SP Sarah Petersen |
22 Jul · experience | Checked in | R 900 | ||
| VILO-2026-GH9921 | JD Jessica Davids |
03–05 Jul · 2n | Completed | R 2 200 | ||
| VILO-2026-IJ7740 | RM Riaan Meyer |
28–30 Jun · 2n | Cancelled | R 2 400 |
Cards
One container pattern, three sizes. Border + radius does the work — no resting shadow.
Spotless, generous breakfast, the host gave us local restaurant tips that turned the weekend around. Will book again next time we're in town.
Thanks Marcus — see you next time. The bay window room is yours whenever you need it.
Upgrade prompt
Free-tier hosts hit feature gates throughout the dashboard. The prompt is inline, never blocking. It explains what's locked, which plan unlocks it, and the price — nothing more.
Direct booking is available on Basic and above.
R299/month — zero booking fees, ever.
Empty states
Every data view needs one. Icon, headline, supportive sentence, optional CTA. No illustrations — they age badly.
No bookings yet
Bookings will appear here once guests start reserving your listing.
Your inbox is empty
Messages from guests will appear here as soon as they reach out.
Toasts
Powered by Sonner. Plain English. Never error codes. Stack top-right on desktop, bottom on mobile.
Notifications panel
Bell-icon trigger in the top nav. Dropdown panel groups by today / earlier. Unread items get a dot + bold weight. Click marks-as-read; “Mark all read” clears the badge.
Listing card
The directory's primary unit. 16:9 image, name + location, rating, price-from, badges. Render the verified + featured badges at most once per card.
Karoo Sunset Walking Tour
Drakensberg Lodge
Booking artifacts
Booking reference, timeline, and price breakdown — three patterns that appear on every booking-related screen.
VILO-YYYY-XXNNNNPolicy & pricing display
Show the rules clearly — guests should never be surprised by a refund outcome. Use a vertical timeline with day thresholds.
-
5+ days before check-in100% refund — no questions asked.
-
1 – 4 days before50% refund — cleaning fee retained.
-
Less than 24 hoursNo refund.
Photos & gallery
Each listing carries up to 20 images. The host editor uses a drag-reorder grid with the first slot pinned as the cover. The public listing page opens a full-screen lightbox.
Payments & EFT
Three rails: Paystack (cards, instant EFT), PayPal (international), and Manual EFT (bank transfer with proof upload). The guest picks one; the host configures which to offer.
Onboarding stepper
5-step host onboarding. The stepper is always visible at the top — never hide progress. Each step is fully validated before "Continue" enables.
Tell us about your first listing
You can add more later — for now, the basics get you set up.
Inbox & messaging
Real-time inbox via Supabase Realtime. Two-pane on desktop, single-pane stack on mobile. Every booking + enquiry creates exactly one thread. System messages are auto-inserted on booking status changes.
Authentication screens
Sign-in / sign-up / forgot password / OAuth / magic link. Split-screen on desktop (brand panel left, form right). Single-column on mobile.
Just a flat subscription.
Branded direct-booking pages, real-time inbox, integrated payments. Built for South African hosts.
Reset your password
We'll email you a reset link.
Map view
Keyless Leaflet + OpenStreetMap with a Wielo-tinted style. Pins are price pills; tap a pin opens a small listing card. Use clustering at low zoom levels.
Error pages
404, 500, offline and feature-gated states. Always offer a way out — never dead-end.
The page you're looking for has moved, been unpublished, or never existed.
We've been notified. Please try again — your data is safe.
Some things won't work until you reconnect. We'll resync automatically.
tailwind.config.ts
Drop this into apps/web/tailwind.config.ts. Same config powers the mobile app via NativeWind — only the content array differs.
// apps/web/tailwind.config.ts import type { Config } from 'tailwindcss'; const config: Config = { content: [ './app/**/*.{ts,tsx}', './components/**/*.{ts,tsx}', './lib/**/*.{ts,tsx}', ], darkMode: 'class', theme: { extend: { colors: { brand: { primary: '#10B981', // emerald — primary actions, links, active nav secondary: '#064E3B', // deep emerald — emphasis, featured, dark CTAs deep: '#064E3B', // alias of secondary accent: '#D1FAE5', // tint surface — hover, selected, badge bg dark: '#0A1510', // near-black — hero / footer / app icon light: '#F0FDF4', // page background ink: '#052E1F', // body text mute: '#4A7C6A', // muted / secondary text line: '#DCEAE0', // borders, dividers }, status: { confirmed: '#10B981', pending: '#F59E0B', cancelled: '#EF4444', completed: '#6366F1', draft: '#94A3B8', }, }, fontFamily: { sans: ['Inter', 'system-ui', 'sans-serif'], display: ['Plus Jakarta Sans', 'sans-serif'], mono: ['JetBrains Mono', 'ui-monospace', 'monospace'], }, borderRadius: { DEFAULT: '10px', // buttons, inputs card: '16px', // cards, panels, modals pill: '9999px', // badges, tags, avatars sm: '6px', // chips, table highlights }, boxShadow: { card: '0 1px 2px rgba(6,78,59,0.04), 0 1px 1px rgba(6,78,59,0.03)', lift: '0 8px 28px -10px rgba(6,78,59,0.14), 0 2px 6px rgba(6,78,59,0.05)', ring: '0 0 0 4px rgba(16,185,129,0.15)', glow: '0 12px 32px -10px rgba(16,185,129,0.35)', }, ringColor: { DEFAULT: '#10B981', }, backgroundImage: { 'brand-gradient': 'linear-gradient(135deg, #10B981 0%, #064E3B 100%)', 'brand-gradient-dark': 'linear-gradient(145deg, #030806 0%, #0a1510 50%, #051209 100%)', }, transitionTimingFunction: { out: 'cubic-bezier(0.2, 0.8, 0.2, 1)', }, }, }, plugins: [ require('@tailwindcss/forms')({ strategy: 'class' }), require('@tailwindcss/typography'), require('tailwindcss-animate'), ], }; export default config;
globals.css
CSS variables for surfaces + shadcn/ui token bridge. Light + dark covered.
/* apps/web/app/globals.css */ @tailwind base; @tailwind components; @tailwind utilities; @layer base { :root { /* Wielo surfaces — used via bg-[var(--brand-surface)] etc. */ --brand-surface: #F0FDF4; --brand-surface-raised: #FFFFFF; --brand-surface-sunken: #E7FAEE; --brand-text: #052E1F; --brand-text-muted: #4A7C6A; --brand-border: #DCEAE0; /* Brand gradient — reserve for logo + one hero element / screen */ --brand-gradient: linear-gradient(135deg, #10B981 0%, #064E3B 100%); --brand-gradient-dark: linear-gradient(145deg, #030806 0%, #0a1510 50%, #051209 100%); /* shadcn/ui token bridge */ --background: 142 76% 97%; --foreground: 158 71% 10%; --card: 0 0% 100%; --card-foreground: 158 71% 10%; --primary: 160 84% 39%; --primary-foreground: 0 0% 100%; --secondary: 161 86% 17%; --secondary-foreground: 0 0% 100%; --muted: 152 76% 90%; --muted-foreground: 158 26% 39%; --accent: 152 76% 90%; --accent-foreground: 158 71% 10%; --destructive: 0 84% 60%; --destructive-foreground: 0 0% 100%; --border: 144 23% 89%; --input: 144 23% 89%; --ring: 160 84% 39%; --radius: 10px; } .dark { --brand-surface: #051209; --brand-surface-raised: #0a1510; --brand-surface-sunken: #030806; --brand-text: #D1FAE5; --brand-text-muted: #4A7C6A; --brand-border: #143324; --background: 158 60% 5%; --foreground: 152 76% 90%; --card: 160 51% 7%; --card-foreground: 152 76% 90%; --primary: 160 84% 39%; --primary-foreground: 158 71% 10%; --secondary: 161 86% 17%; --secondary-foreground: 152 76% 90%; --muted: 158 36% 12%; --muted-foreground: 158 18% 56%; --accent: 158 36% 12%; --accent-foreground: 152 76% 90%; --destructive: 0 84% 60%; --destructive-foreground: 0 0% 100%; --border: 158 36% 14%; --input: 158 36% 14%; --ring: 160 84% 39%; } html, body { background: var(--brand-surface); color: var(--brand-text); font-family: 'Inter', system-ui, sans-serif; text-rendering: optimizeLegibility; -webkit-font-smoothing: antialiased; } h1, h2, h3 { font-family: 'Plus Jakarta Sans', sans-serif; letter-spacing: -0.01em; } /* Brand gradient utility — for the logo and ONE hero surface per screen */ .bg-brand-gradient { background: var(--brand-gradient); } .bg-brand-gradient-dark { background: var(--brand-gradient-dark); } @media (prefers-reduced-motion: reduce) { * { animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; } } }
Dark mode preview
Toggled via Tailwind's class strategy. Surfaces invert; the emerald primary stays the same so brand recognition holds in both modes.
Mobile screens
Reference screens in a phone bezel, showing how the same tokens compose on small viewports. NativeWind uses identical class names — these designs ship 1:1 to React Native.
Do & don't
Pin these somewhere visible. They cover ~90% of the design decisions you'll hit while building.
- Use
bg-brand-primaryfor primary actions, links, active nav. - Reach for amber (
brand.secondary) only for featured/promo/price emphasis. - Use status tones for booking statuses only — never for unrelated success/warning UI.
- Format currency through the shared
formatCurrency()util — never raw numbers. - Build forms with React Hook Form + Zod, no manual
useState. - Match shadcn/ui sizing — extend via wrappers, never edit
components/ui/. - Design mobile-first. Add
lg:overrides for desktop, never the reverse. - Use flex/grid +
gap-*for layout — not per-element margins.
- Don't use raw Tailwind colours (
blue-500,green-600) for brand purposes. - Don't stack shadows —
shadow-cardat rest,shadow-lifton hover, that's it. - Don't put gradients on backgrounds or buttons. Flat surfaces only.
- Don't use font sizes below 12px or weight above 700.
- Don't show technical error messages in toasts — Sentry gets the stack, users get plain English.
- Don't reach for spinners as the default loading state — match the shape of the real content with skeletons.
- Don't store remote data in Zustand. Supabase reads go through TanStack Query.
- Don't block free-tier hosts with modals — show the inline upgrade prompt instead.
DESIGN_SYSTEM.md, then keep building.