🏘️ Version 1.0 · Production Ready

Design with neighborhood at heart

The Ubani Design System is the single source of truth for building warm, accessible, and trustworthy experiences for the neighborhood social hub — where real neighbors connect.

30+Components
200+Tokens
9Type Levels
WCAG AAAccessible
Design Principles
🌿
Rooted & Trustworthy
Every interaction should feel like talking to a neighbor you know and trust. We avoid dark patterns, use honest copy, and design for clarity over cleverness. Ubani earns trust at every touchpoint.
🤝
Inclusive by Default
Neighborhoods are diverse. Our system meets WCAG 2.1 AA minimums (targeting AAA for body text), supports right-to-left layouts, works on older devices, and is designed for all ages — from Gen-Z to retirees.
🌱
Human & Warm
We use earthy greens and clay tones — colors borrowed from gardens, soil, and morning light. Typography is set in Pally Variable (display) for warmth and DM Sans (body) for clarity. Never cold or corporate.
⚡️
Efficient & Focused
Neighbors are busy. We reduce cognitive load through consistent patterns, clear hierarchy, and purposeful whitespace. Motion is used sparingly — only when it communicates meaning, not decoration.
📍
Local-First Mindset
Everything is anchored to place. Neighborhood names are always prominent. Map elements, local tags, and geographic context are first-class design elements in the system.
🔒
Safety by Design
Community safety is paramount. Content moderation states, reporting flows, and privacy indicators have dedicated components and patterns. We make safe choices feel natural, not laborious.
Do's & Don'ts

✅ Do — Use brand green purposefully

Reserve the primary green for the single most important action per screen. One CTA rules. Use secondary and ghost variants for supporting actions.

🚫 Don't — Compete for attention

Don't place two primary buttons side by side. Never use green for decorative purposes unrelated to action or status.

✅ Do — Lead with neighborhood context

Always show the posting neighborhood near the author. Help users immediately understand the geographic relevance of content.

🚫 Don't — Strip location context

Never show posts without neighborhood attribution. Location is the core trust signal in Ubani.

✅ Do — Use Pally Variable for headings

The display font brings warmth and personality to section titles, hero text, and large callouts. It reflects the humanist brand voice.

🚫 Don't — Use display font for body

Pally Variable at small sizes reduces readability. Use DM Sans for all body text, labels, captions, and interactive elements.

Foundations

Color System

A warm, nature-inspired palette anchored in trust and community. Every color has a purpose. The system scales from primitive values to semantic tokens to component-level usage.

Brand Green — Primary
Green 50
#F7FEE7
Green 100
#ECFCCB
Green 200
#D9F99D
Green 300
#BEF264
Green 400
#A3E635
Green 500 ★
#84CC16
Green 600
#65A30D
Green 700
#4D7C0F
Green 800
#3F6212
Green 900
#365314
Green 950
#1A2E05
Clay — Secondary
Clay 50
#FDF8F3
Clay 100
#F8EEE2
Clay 200
#EFDBCA
Clay 400
#D2A478
Clay 500 ★
#C08450
Clay 600
#A36840
Clay 700
#804F2F
Clay 800
#5A3720
Semantic Colors
Info
#D6EEFF / #062C57
Warning
#FFF3CC / #4F3100
Error
#960F0F
Success
#ECFCCB / #365314
Neutral Scale
White
#FFFFFF
N-50
#F7F6F4
N-100
#EDECEA
N-200
#DDDCD9
N-500
#908D86
N-600
#706D67
N-800
#353330
N-900
#1C1A18
N-1000
#0A0908
Contrast Ratios (WCAG)
ForegroundBackgroundRatioAA NormalAA LargeAAA
#1C1A18 (N-900)#FFFFFF17.4:1PASSPASSPASS
#4D7C0F (Green 700)#FFFFFF5.0:1PASSPASSFAIL
#65A30D (Green 600)#FFFFFF3.1:1FAILPASSFAIL
#84CC16 (Green 500)#FFFFFF2.0:1FAILFAILFAIL
#FFFFFF (White)#84CC16 (Green 500)2.0:1FAILFAILFAIL
#1C1A18 (N-900)#84CC16 (Green 500)8.8:1PASSPASSPASS
#706D67 (N-600)#FFFFFF5.2:1PASSPASSFAIL
#AEABA5 (N-400)#FFFFFF2.3:1FAILFAILFAIL

⚠️ Primary guidance: use dark text (#1C1A18) on Green 500 for AA/AAA. N-400 fails and is decorative only.

Foundations

Typography

Two typefaces. Nine scales. Every decision is rooted in readability and warmth — Pally Variable for soul, DM Sans for clarity, DM Mono for code and data.

Type Scale — 9 Levels
2XS
10px / 0.625rem
Label · Caption · Overline
XS
12px / 0.75rem
Badge · Tag · Helper text · Timestamp
SM
14px / 0.875rem
Body SM · Secondary text · Captions
BASE ★
16px / 1rem
Body · Default reading size · Most paragraph text
LG
18px / 1.125rem
Intro text · Large body · Lead paragraphs
XL
20px / 1.25rem
Sub-heading · Card title · Nav title
2XL
24px / 1.5rem
Section Heading — Pally Variable Medium
3XL
32px / 2rem
Page Title — Pally Variable Light
4XL
44px / 2.75rem
Hero
Typefaces
Pally Variable
Display & Headings
Role
All H1–H3, hero, section titles
Weights
300, 500, 700
Style
Optical size 9–144, italic available
DM Sans
Body & UI
Role
Body, buttons, labels, inputs, nav
Weights
300, 400, 500, 600
Style
Optical size 9–40, italic available
DM Mono
Code & Data
Role
Code blocks, tokens, data values
Weights
400, 500
Accessibility: Minimum Sizes
WCAG 1.4.4 — Resize Text
All text must remain readable at 200% browser zoom. Never use px for line-height — always unitless multiples. Minimum interactive text: 14px (0.875rem). Body text minimum: 16px. Never disable user font scaling.
Foundations

Layout & Grid

A 12-column grid with 8px base spacing creates consistent, flexible layouts at every breakpoint. Every measurement in the system is a multiple of 8px.

12-Column Grid
1
2
3
4
5
6
7
8
9
10
11
12
3 col
9 col
4 col
4 col
4 col
6 col
6 col
8 col
4 col
Columns
12
Gutter
24px (space-6)
Margin
24px mobile, 40px desktop
Max width
1280px
Content max
960px
Prose max
680px
Mobile (< 600)
4 columns
Tablet (600–1024)
8 columns
Desktop (>1024)
12 columns
8px Spacing Scale
--space-1 · 4px · 0.5× · Inline gaps, icon padding
--space-2 · 8px · 1× · Base unit · Tight spacing
--space-3 · 12px · 1.5× · Component internal padding
--space-4 · 16px · 2× · Card padding · Vertical rhythm
--space-6 · 24px · 3× · Section padding · Grid gutter
--space-8 · 32px · 4× · Between sections
--space-12 · 48px · 6× · Section breaks
--space-16 · 64px · 8× · Page sections
--space-20 · 80px · 10× · Hero padding
Components

Component Library

30+ production-ready components, each with states, anatomy, accessibility notes, and CSS specs. All components support dark mode and WCAG AA.

Actions & Inputs
Components 01-05
01 — Button
Button
The primary interactive trigger. Five variants (primary, secondary, ghost, danger, clay) × four sizes (sm, md, lg, xl) × icon-only mode.
Touch target
Minimum 44×44px (WCAG 2.5.5)
Focus
3px green ring, 2px offset
Disabled
opacity via muted bg, cursor:not-allowed
Loading state
Replace label with spinner + "Loading…" aria-live
Accessibility
role="button", aria-disabled, aria-pressed for toggles
.btn .btn-{variant} .btn-{size}
02 — Avatar
MK
JS
RW
AT
LP
B
Avatar Group
MK
JS
RW
+4
Avatar
Represents a user with initials or image. Six sizes (xs–2xl). Color variants map to neighborhood identity. Group variant shows multiple users with overlap.
Sizes
24, 32, 40, 48, 64, 80px
Colors
Green (default), Clay, Sky, Amber
Image fallback
First initial(s) on branded bg
Accessibility
alt text for img, aria-label for initials
03 — Badge
✅ Verified Neighbor 🛒 Marketplace 📢 Announcement ⚠️ Safety Alert 🚨 Urgent General New Online Offline
Badge
Status and category labels. Used for content categories, verification states, and counts. Dot variant adds status indicator.
Anatomy
Icon · Label text · (optional dot)
Max chars
24 characters recommended
Accessibility
Avoid conveying status by color alone; include text
04 — Form Inputs
As it appears on your ID
📍
⚠ This field is required
Input / Form
Covers text input, textarea, select, checkbox, radio, toggle. States: default, hover, focus, filled, error, disabled.
Height
SM: 32px · MD: 40px · LG: 48px
Border radius
var(--radius-lg) = 12px
Focus ring
Green-400 border + 3px rgba shadow
Error
Red border, aria-invalid, aria-describedby linked error msg
Accessibility
Always associate label via for/id. Never use placeholder as label.
05 — Checkbox, Radio, Toggle
Notifications
Public profile
Selection Controls
Checkbox for multi-select, radio for single-select from a set, toggle for immediate binary settings.
Checkbox size
18×18px with 44px click target
Toggle
44×24px track, 20px thumb
role for toggle
role="switch" + aria-checked
Social & Community
Components 06-09
06 — Post Card
MK
Has anyone seen a black lab near Pine Street? He answers to "Biscuit" and has been missing since this morning. Please message me if you spot him! 🐕
📷 Photo · tap to expand
❤️ 12
👍 8
🙏 5
+
Post Card
The primary content unit. Shows author avatar, neighborhood context, category badge, body text, optional media, reactions, and actions.
Anatomy
Header · Body · Media · Reactions · Actions
States
Default, hover, pinned, urgent, hidden (moderated)
Categories
Safety, Event, Lost & Found, Marketplace, Discussion, Alert
Accessibility
article role, heading structure, action aria-labels
07 — Neighborhood Card
Maple Heights
📍 0.3 mi·842 neighbors
Riverside Commons
📍 0.8 mi·1.2K neighbors
Neighborhood Card
Represents a local neighborhood with member count, distance, and join CTA. Banner color customizable per neighborhood identity.
States
Available, Joined, Pending verification
08 — Alert & Toast
You're now a neighbor!
Welcome to Maple Heights. You can now see and post in your neighborhood.
⚠️
Address unverified
Verify your address to unlock full neighborhood access.
🚨
Post removed
This post was removed for violating community guidelines.
✉️Message sent to your neighbor
Alert & Toast
Alerts are persistent in-page feedback. Toasts are transient notifications that auto-dismiss after 5 seconds.
Alert types
Success, Info, Warning, Error
Toast duration
5000ms default, pausable on hover
Accessibility
role="alert" for urgent, role="status" for polite. Toast: aria-live="polite"
09 — Modal
Modal
Focused overlay for confirmation dialogs, forms, and detailed views. Always includes a visible close affordance.
Max width
480px (SM) / 720px (LG)
Backdrop
50% neutral + blur(4px)
Accessibility
role="dialog", aria-modal="true", focus trap, Esc to close
Navigation & Discovery
Components 10-14
10 — Search Bar
Search Bar
Global and scoped search. Supports keyboard shortcut display, voice input icon, and auto-suggestion dropdown.
Accessibility
type="search", role="combobox" when expanded, aria-autocomplete
11 — Tabs
Tab panel content goes here.
Tabs
Line tabs for primary page navigation. Pill tabs for filter/segment switching within a section.
Keyboard
Arrow keys navigate, Enter selects, Home/End jump
Accessibility
role="tablist/tab/tabpanel", aria-selected, aria-controls
12 — Tags & Filter Chips
🔥 Safety 🎉 Events 🛒 Marketplace 🐾 Lost & Found 💡 Recommendations
Python Heights Within 1mi Last 7 days
Tag / Filter Chip
Scannable labels for categories and active filters. Dismissible variant removes the filter when × is tapped.
States
Default, hover, selected, disabled
13 — Dropdown Menu
Dropdown Menu
Contextual action menu triggered by a button or icon. Supports icons, dividers, and destructive action styling.
Accessibility
role="menu/menuitem", aria-expanded, keyboard navigation (↑↓, Esc)
14 — Notification Item
JR
James R. replied to your post about the block party.
5 min ago
🚨
Safety Alert: Road closure on Pine Ave until Friday.
1 hour ago
NK
Nadia K. thanked you for your recommendation.
Yesterday
Notification
Activity feed item. Unread state uses subtle green tint + dot indicator. Always shows avatar, action text, and relative timestamp.
States
Read, Unread, Alert (red dot)
Accessibility
aria-label for dot ("unread notification"); role="listitem"
Feedback & System Status
Components 15-21
15 — Skeleton Loader
Skeleton Loader
Placeholder shown while content loads. Reduces perceived wait time vs spinners. Always match the shape of real content.
Animation
Shimmer, 1.5s infinite, left→right
Accessibility
aria-busy="true" on container; hide from screen readers
16 — Progress Bar
Profile completion73%
Event capacity45/60
Verification pendingStep 2 of 3
Progress Bar
Indicates completion of a process or percentage of a capacity. Sizes: 4px (compact), 8px (default), 12px (prominent).
Accessibility
role="progressbar", aria-valuenow, aria-valuemin, aria-valuemax
17 — Stat Card
Neighbors
842
+12 this week
Posts Today
34
+8 vs yesterday
Events
6
This month
Stat Card
At-a-glance metric for neighborhood dashboards and admin views. Pally Variable for the value communicates warmth even in data.
18 — Accordion
We send a postcard to your address with a 6-digit code. Enter this code in your profile to become a Verified Neighbor. This takes 3–5 business days.
Yes! You can join up to 3 neighborhoods. Your primary neighborhood gets full access; adjacent neighborhoods have read-only access.
Your data is never sold. Location data is only used to show relevant neighborhood content. Read our full privacy policy for details.
Accordion
Progressive disclosure for FAQ, settings, and detailed descriptions. Supports single and multi-open variants.
Accessibility
aria-expanded, aria-controls, role="region" for panels
19 — Tooltip
Appears on hover — 200ms delay
Verification required to post
Tooltip
Non-interactive helper text. 200ms appear delay to prevent accidental triggers. Max 80 chars. Never put interactive content in a tooltip.
Positions
Top (default), bottom, left, right
Accessibility
role="tooltip", aria-describedby, keyboard accessible
20 — Poll
What should we prioritize for the Maple Heights park renovation?
New playground equipment 48%
Better lighting 31%
Community garden space 21%
Poll
Neighborhood voting tool with result bars. Pre-vote shows options. Post-vote shows percentage fill animation.
Accessibility
fieldset + legend; radio buttons; progress visualization described
21 — Empty State
🏘️
No posts yet
Be the first to share something with your neighborhood. Say hello, ask a question, or post a local tip!
Empty State
Friendly zero-state screens that reduce frustration. Always includes an icon, title, description, and a clear CTA to take action.
Use cases
No posts, no results, no notifications, offline, error, no permission
Information Architecture & Utility
Components 22-30
22 — Breadcrumb
Breadcrumb
Location context for deep navigation. Always includes aria-label="Breadcrumb" and aria-current="page" on active item.
23 — Pagination
Pagination
Used for long list navigation. Supports truncation with ellipsis for large page counts.
24 — Callout
🌿
Verified Neighbor Benefit
Post anonymously, access private boards, and vote in local elections once verified.
📝
Posting Guidelines
Keep content local, respectful, and relevant to your neighborhood.
Callout
Inline information blocks with higher prominence than body text. Four types: tip (sky), note (amber), warning (red), feature (green).
25 — User Card
MK
Maya Kowalski ✓ Verified
📍 Maple Heights · Member since 2023
Dog mom, gardener, and block party organizer. Always happy to recommend a great local restaurant!
User Card
Neighbor profile preview shown in search, recommendations, and hover states. Includes verification badge and quick actions.
26 — Comment
JR
James R. · 2h ago
I saw Biscuit near the corner of Pine and Oak around noon! He looked healthy and was heading east.
MK
Maya K. · 1h ago
Thank you so much! We found him! 🐕🎉
Comment
Threaded reply system with nested indent (max 2 levels). Avatar + author + timestamp + text + actions.
27 — Map Card
📍 Interactive map preview
Maple Heights Park
0.3 mi · 2920 Maple Ave
Map Card
Shows a location preview with pulsing pin indicator. Used for events, lost pets, local business, and safety incidents.
28 — Bottom Navigation (Mobile)
App content area
Bottom Nav
Primary navigation for mobile (< 768px). Floating action "Post" button is always center. Respects safe area insets. Max 5 items.
Touch target
Minimum 44×44px per item
Accessibility
nav role, aria-current="page" on active item
29 — Share Sheet
Share Sheet
Bottom sheet for sharing content within or outside the app. Uses native Share API on mobile when available.
30 — Divider
or continue with
Divider
Simple horizontal rule or labeled separator. Used between sections, in forms, and in menus. role="separator" when semantic.
Patterns

UX Patterns

Repeatable interaction patterns that combine components into cohesive user flows. Follow these to maintain consistency across features.

🏠
Feed Pattern
Infinite scroll list of post cards. Tabs above for category filtering. Floating compose button (mobile) or sticky sidebar "What's on your mind?" card (desktop). Pull-to-refresh on mobile.
Post CardTabsFilter ChipsSkeleton
✍️
Compose Flow
Modal-based composer. Step 1: Category + title. Step 2: Body text + media. Step 3: Neighborhood scope (mine / adjacent). Preview before publish. Character counter.
ModalInputProgressButton
🔐
Verification Flow
3-step onboarding: (1) Enter address → (2) Confirm via postcard code → (3) Verification badge awarded. Progress bar shows step. Callout reminds of privacy policy.
ProgressAlertCalloutBadge
🔎
Search & Discovery
Search bar with instant results panel. Tabs: Posts / Neighbors / Events / Businesses. Filter chips refine by distance, category, and time. Empty state with suggestions if no results.
Search BarTabsTagsEmpty State
🚩
Reporting Flow
Triggered from post/comment ⋯ menu. Modal with reason select + optional detail. Confirmation toast. Hides content immediately (optimistic). Admin review in background.
DropdownModalToast
🔔
Notification Center
Slide-in panel (desktop) or full screen (mobile). Grouped by day. Unread count badge on nav icon. "Mark all read" action. Notification items link to relevant content.
NotificationBadgeTabs
🗺️
Neighborhood Map View
Full-screen map with clustered pins by category. Map card previews on pin tap. Filter bar overlays map. List/Map toggle button. Location permission request callout.
Map CardFilter ChipsCallout
Safety Alert Broadcast
Special urgent post type. Red banner at top of feed. Push notification with red badge. Alert card has "I'm Safe" CTA. Auto-expires after 48h if no confirmation. Admin-managed.
AlertBadgeToast
Interaction Principles

✅ Optimistic Updates

Apply state changes immediately (likes, joins) and roll back on API failure. Neighbors feel a snappy experience even on slow connections.

🚫 Blocking Loaders

Don't block the entire UI with a spinner for network requests. Use skeleton loaders for content, toast for confirmations.

✅ Confirm Destructive Actions

Always confirm delete, block, or report actions via a dialog. Use red button + explicit warning text. Never trigger irreversible actions on single click.

🚫 Auto-submit Forms

Never submit forms without explicit user action. Debounce search but don't auto-submit report or post forms. Neighbors need control.

Tokens

Design Tokens JSON

Machine-readable token definitions. Use with Style Dictionary, Theo, or Tailwind config to generate platform-specific outputs (CSS, iOS, Android).

💡
Token Naming Convention
Format: [category]-[property]-[modifier]. Example: color-bg-primary-subtle. All tokens map to a primitive value. Never reference another semantic token from a semantic token.
{ "meta": { "name": "Ubani Design System", "version": "1.0.0", "description": "Neighborhood social hub — neighborhood-first, warm, accessible." }, "color": { "brand": { "green": { "50": { "value": "#F7FEE7", "type": "color" }, "100": { "value": "#ECFCCB", "type": "color" }, "200": { "value": "#D9F99D", "type": "color" }, "300": { "value": "#BEF264", "type": "color" }, "400": { "value": "#A3E635", "type": "color" }, "500": { "value": "#84CC16", "type": "color", "comment": "Primary brand green" }, "600": { "value": "#65A30D", "type": "color" }, "700": { "value": "#4D7C0F", "type": "color" }, "800": { "value": "#3F6212", "type": "color" }, "900": { "value": "#365314", "type": "color" }, "950": { "value": "#1A2E05", "type": "color" } }, "clay": { "500": { "value": "#C08450", "type": "color", "comment": "Secondary brand clay" }, "600": { "value": "#A36840", "type": "color" }, "700": { "value": "#804F2F", "type": "color" } } }, "semantic": { "bg": { "base": { "value": "{color.neutral.0}", "type": "color" }, "subtle": { "value": "{color.neutral.50}", "type": "color" }, "muted": { "value": "{color.neutral.100}", "type": "color" }, "primary": { "value": "{color.brand.green.500}", "type": "color" }, "primary-subtle": { "value": "{color.brand.green.50}", "type": "color" }, "error": { "value": "{color.status.red.50}", "type": "color" }, "warning": { "value": "{color.status.amber.50}", "type": "color" }, "success": { "value": "{color.brand.green.50}", "type": "color" }, "info": { "value": "{color.status.sky.50}", "type": "color" } }, "text": { "primary": { "value": "{color.neutral.900}", "type": "color" }, "secondary": { "value": "{color.neutral.600}", "type": "color" }, "tertiary": { "value": "{color.neutral.400}", "type": "color" }, "inverse": { "value": "{color.neutral.0}", "type": "color" }, "brand": { "value": "{color.brand.green.600}", "type": "color" }, "error": { "value": "{color.status.red.600}", "type": "color" } }, "border": { "default": { "value": "{color.neutral.200}", "type": "color" }, "subtle": { "value": "{color.neutral.100}", "type": "color" }, "strong": { "value": "{color.neutral.400}", "type": "color" }, "brand": { "value": "{color.brand.green.400}", "type": "color" }, "error": { "value": "{color.status.red.400}", "type": "color" } } } }, "typography": { "fontFamily": { "display": { "value": "'Pally Variable', Georgia, serif", "type": "fontFamily" }, "body": { "value": "'DM Sans', system-ui, sans-serif", "type": "fontFamily" }, "mono": { "value": "'DM Mono', 'Fira Code', monospace", "type": "fontFamily" } }, "fontSize": { "2xs": { "value": "0.625rem", "type": "fontSize", "comment": "10px" }, "xs": { "value": "0.75rem", "type": "fontSize", "comment": "12px" }, "sm": { "value": "0.875rem", "type": "fontSize", "comment": "14px" }, "base": { "value": "1rem", "type": "fontSize", "comment": "16px — default body" }, "lg": { "value": "1.125rem", "type": "fontSize", "comment": "18px" }, "xl": { "value": "1.25rem", "type": "fontSize", "comment": "20px" }, "2xl": { "value": "1.5rem", "type": "fontSize", "comment": "24px" }, "3xl": { "value": "2rem", "type": "fontSize", "comment": "32px" }, "4xl": { "value": "2.75rem", "type": "fontSize", "comment": "44px" }, "5xl": { "value": "3.75rem", "type": "fontSize", "comment": "60px — hero only" } }, "lineHeight": { "tight": { "value": "1.15", "type": "lineHeight" }, "snug": { "value": "1.3", "type": "lineHeight" }, "normal": { "value": "1.5", "type": "lineHeight" }, "relaxed": { "value": "1.7", "type": "lineHeight" } }, "fontWeight": { "light": { "value": "300", "type": "fontWeight" }, "regular": { "value": "400", "type": "fontWeight" }, "medium": { "value": "500", "type": "fontWeight" }, "semibold": { "value": "600", "type": "fontWeight" } } }, "spacing": { "0": { "value": "0px", "type": "spacing" }, "1": { "value": "4px", "type": "spacing" }, "2": { "value": "8px", "type": "spacing", "comment": "1 base unit" }, "3": { "value": "12px", "type": "spacing" }, "4": { "value": "16px", "type": "spacing" }, "5": { "value": "20px", "type": "spacing" }, "6": { "value": "24px", "type": "spacing" }, "8": { "value": "32px", "type": "spacing" }, "10": { "value": "40px", "type": "spacing" }, "12": { "value": "48px", "type": "spacing" }, "16": { "value": "64px", "type": "spacing" }, "20": { "value": "80px", "type": "spacing" }, "24": { "value": "96px", "type": "spacing" } }, "borderRadius": { "none": { "value": "0px", "type": "borderRadius" }, "sm": { "value": "4px", "type": "borderRadius" }, "md": { "value": "8px", "type": "borderRadius" }, "lg": { "value": "12px", "type": "borderRadius" }, "xl": { "value": "16px", "type": "borderRadius" }, "2xl": { "value": "24px", "type": "borderRadius" }, "full": { "value": "9999px", "type": "borderRadius" } }, "shadow": { "xs": { "value": "0 1px 2px rgba(28,26,24,0.06)", "type": "shadow" }, "sm": { "value": "0 1px 4px rgba(28,26,24,0.08), 0 0 1px rgba(28,26,24,0.04)", "type": "shadow" }, "md": { "value": "0 4px 12px rgba(28,26,24,0.1), 0 0 1px rgba(28,26,24,0.06)", "type": "shadow" }, "lg": { "value": "0 8px 24px rgba(28,26,24,0.12), 0 0 2px rgba(28,26,24,0.06)", "type": "shadow" }, "xl": { "value": "0 16px 48px rgba(28,26,24,0.16), 0 0 2px rgba(28,26,24,0.08)", "type": "shadow" }, "focus": { "value": "0 0 0 3px rgba(132,204,22,0.3)", "type": "shadow", "comment": "Focus ring — use on all interactive elements" } }, "motion": { "duration": { "instant": { "value": "80ms", "type": "duration" }, "fast": { "value": "150ms", "type": "duration" }, "normal": { "value": "250ms", "type": "duration" }, "slow": { "value": "400ms", "type": "duration" } }, "easing": { "out": { "value": "cubic-bezier(0.16, 1, 0.3, 1)", "type": "easing" }, "in": { "value": "cubic-bezier(0.4, 0, 1, 1)", "type": "easing" }, "inout": { "value": "cubic-bezier(0.4, 0, 0.2, 1)", "type": "easing" }, "spring": { "value": "cubic-bezier(0.34, 1.56, 0.64, 1)", "type": "easing" } } }, "zIndex": { "below": { "value": "-1", "type": "other" }, "base": { "value": "0", "type": "other" }, "raised": { "value": "10", "type": "other" }, "overlay": { "value": "100", "type": "other" }, "modal": { "value": "200", "type": "other" }, "toast": { "value": "300", "type": "other" }, "tooltip": { "value": "400", "type": "other" } } }
Development

Developer Guide

Everything engineers need to implement the Ubani Design System consistently, efficiently, and accessibly.

Setup & Installation
/* 1. Import fonts in <head> */ <link rel="preconnect" href="https://fonts.googleapis.com"> <link href="https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,300;0,9..40,400;0,9..40,500;0,9..40,600;1,9..40,400&family=DM+Mono:wght@400;500&display=swap" rel="stylesheet"> <link href="https://api.fontshare.com/v2/css?f[]=pally@1,2,3,4,5,6,7&display=swap" rel="stylesheet"> /* 2. Import design tokens CSS (generated from tokens.json via Style Dictionary) */ @import '@ubani/design-tokens/css/variables.css'; /* 3. For dark mode — add data-theme="dark" to <html> */ document.documentElement.setAttribute('data-theme', 'dark');
CSS Custom Properties Usage
/* ✅ DO — Always use semantic tokens in components */ .card { background: var(--color-bg-base); border: 1px solid var(--color-border-default); border-radius: var(--radius-xl); padding: var(--space-6); box-shadow: var(--shadow-sm); } /* ✅ DO — Use primitive tokens only in design-tokens layer */ :root { --color-bg-base: var(--sz-neutral-0); } /* 🚫 NEVER use raw hex values in component styles */ .card { background: #FFFFFF; /* Wrong — breaks dark mode */ border: 1px solid #DDDCD9; /* Wrong */ }
Component Class Convention
/* BEM-inspired with Ubani prefix */ /* Block */ .sz-post-card { } /* Element */ .sz-post-card__header { } .sz-post-card__author { } .sz-post-card__body { } .sz-post-card__actions { } /* Modifier */ .sz-post-card--pinned { } .sz-post-card--urgent { } .sz-post-card--loading { } /* Utility classes */ .sz-sr-only { /* screen reader only */ } .sz-text-primary { color: var(--color-text-primary); } .sz-bg-subtle { background: var(--color-bg-subtle); }
Accessibility Checklist
All interactive elements reachable by keyboard (Tab, Enter, Space, Esc)
Focus visible on all interactive elements — never outline: none without custom focus style
All images have descriptive alt text. Decorative images: alt=""
Color is never the sole conveyor of information
Minimum contrast: 4.5:1 for normal text, 3:1 for large text
All form inputs have visible labels (not just placeholders)
Modals trap focus. Esc closes. Focus returns to trigger on close.
Loading states announced: aria-live="polite" or aria-busy="true"
Respect prefers-reduced-motion: disable/reduce animations
Touch targets: minimum 44×44px on mobile (WCAG 2.5.5)
/* Reduced motion support — always include */ @media (prefers-reduced-motion: reduce) { *, *::before, *::after { animation-duration: 0.01ms !important; animation-iteration-count: 1 !important; transition-duration: 0.01ms !important; } } /* Dark mode with system preference fallback */ @media (prefers-color-scheme: dark) { :root:not([data-theme="light"]) { /* Apply dark tokens */ } }
React Component Example
import React from 'react'; interface ButtonProps { variant?: 'primary' | 'secondary' | 'ghost' | 'danger' | 'clay'; size?: 'sm' | 'md' | 'lg' | 'xl'; loading?: boolean; disabled?: boolean; onClick?: () => void; children: React.ReactNode; 'aria-label'?: string; } export const Button: React.FC<ButtonProps> = ({ variant = 'primary', size = 'md', loading = false, disabled = false, onClick, children, 'aria-label': ariaLabel, }) => { return ( <button className={`btn btn-${variant} btn-${size}`} onClick={onClick} disabled={disabled || loading} aria-disabled={disabled || loading} aria-label={ariaLabel} aria-busy={loading} > {loading ? ( <> <span className="sz-sr-only">Loading…</span> <span aria-hidden="true">⟳</span> </> ) : children} </button> ); };
Breakpoints Reference
/* Ubani Breakpoints */ --bp-xs: 480px; /* Small phones */ --bp-sm: 600px; /* Large phones */ --bp-md: 768px; /* Tablets — nav changes here */ --bp-lg: 1024px; /* Desktop — sidebar appears */ --bp-xl: 1280px; /* Wide desktop — max grid width */ /* Usage in CSS */ @media (max-width: 767px) { /* Mobile */ } @media (min-width: 768px) { /* Tablet+ */ } @media (min-width: 1024px) { /* Desktop+ */ } /* Usage in JS / Tailwind config */ screens: { 'xs': '480px', 'sm': '600px', 'md': '768px', 'lg': '1024px', 'xl': '1280px', }
Dark Mode Implementation
// React dark mode hook function useDarkMode() { const [isDark, setIsDark] = React.useState(() => { const saved = localStorage.getItem('sz-theme'); if (saved) return saved === 'dark'; return window.matchMedia('(prefers-color-scheme: dark)').matches; }); React.useEffect(() => { document.documentElement.setAttribute('data-theme', isDark ? 'dark' : 'light'); localStorage.setItem('sz-theme', isDark ? 'dark' : 'light'); }, [isDark]); return { isDark, toggle: () => setIsDark(prev => !prev) }; }
📦
Package Distribution
Tokens are published as @ubani/design-tokens (CSS + JSON + iOS Swift + Android XML). Components ship as @ubani/ui (React). Figma variables sync via the Tokens Studio plugin. Contact the Design Systems team at design-systems@ubani.com for access.