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.