UI/UX design workflow guidelines. Activate when working with design systems, accessibility (WCAG), user interface patterns, or design tokens.
The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in RFC 2119.
Accessibility is NOT optional. All implementations MUST achieve these standards.
// Focus trap for modals - REQUIRED
const useFocusTrap = (containerRef: RefObject<HTMLElement>) => {
useEffect(() => {
const container = containerRef.current;
if (!container) return;
const focusables = container.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
const first = focusables[0] as HTMLElement;
const last = focusables[focusables.length - 1] as HTMLElement;
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key !== 'Tab') return;
if (e.shiftKey && document.activeElement === first) {
e.preventDefault(); last.focus();
} else if (!e.shiftKey && document.activeElement === last) {
e.preventDefault(); first.focus();
}
};
container.addEventListener('keydown', handleKeyDown);
first?.focus();
return () => container.removeEventListener('keydown', handleKeyDown);
}, [containerRef]);
};
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
Run: npx lighthouse <url> --only-categories=accessibility
Design tokens MUST be the single source of truth for visual styling.
:root {
/* Colors - Semantic */
--color-primary: #2563eb;
--color-primary-hover: #1d4ed8;
--color-secondary: #64748b;
--color-success: #16a34a;
--color-warning: #ca8a04;
--color-error: #dc2626;
/* Colors - Neutral */
--color-neutral-50: #fafafa;
--color-neutral-100: #f4f4f5;
--color-neutral-200: #e4e4e7;
--color-neutral-700: #3f3f46;
--color-neutral-800: #27272a;
--color-neutral-900: #18181b;
/* Spacing - 4px base */
--space-1: 0.25rem; --space-2: 0.5rem; --space-3: 0.75rem;
--space-4: 1rem; --space-6: 1.5rem; --space-8: 2rem;
--space-12: 3rem; --space-16: 4rem;
/* Typography */
--font-sans: system-ui, -apple-system, sans-serif;
--font-mono: ui-monospace, 'Cascadia Code', monospace;
--text-xs: 0.75rem; --text-sm: 0.875rem; --text-base: 1rem;
--text-lg: 1.125rem; --text-xl: 1.25rem; --text-2xl: 1.5rem;
--leading-tight: 1.25; --leading-normal: 1.5; --leading-relaxed: 1.75;
}
:root {
--bg-primary: #ffffff;
--bg-secondary: #f4f4f5;
--text-primary: #18181b;
--text-secondary: #52525b;
--border-color: #e4e4e7;
}
:root.dark {
--bg-primary: #18181b;
--bg-secondary: #27272a;
--text-primary: #fafafa;
--text-secondary: #a1a1aa;
--border-color: #3f3f46;
}
@media (prefers-color-scheme: dark) {
:root:not(.light) { /* Apply dark values */ }
}
color-scheme: dark for native elements.btn {
display: inline-flex; align-items: center; justify-content: center;
gap: var(--space-2); padding: var(--space-2) var(--space-4);
font-weight: 500; border-radius: 0.375rem;
transition: background-color 150ms, box-shadow 150ms;
}
.btn:focus-visible { outline: 2px solid var(--color-primary); outline-offset: 2px; }
.btn:disabled { opacity: 0.5; cursor: not-allowed; }
<div class="form-field">
<label for="email">Email address</label>
<input type="email" id="email" aria-describedby="email-error" aria-invalid="true" />
<span id="email-error" role="alert">Please enter a valid email</span>
</div>
<dialog> element when possibleAnimation SHOULD be subtle and purposeful. Never use motion for decoration.
:root {
--ease-out: cubic-bezier(0, 0, 0.2, 1);
--ease-in-out: cubic-bezier(0.4, 0, 0.2, 1);
--duration-fast: 150ms; --duration-normal: 200ms; --duration-slow: 300ms;
}
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
@keyframes slideUp { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } }
Mobile-first approach is REQUIRED.
:root { --bp-sm: 640px; --bp-md: 768px; --bp-lg: 1024px; --bp-xl: 1280px; }
.container { padding: var(--space-4); }
@media (min-width: 768px) { .container { padding: var(--space-6); } }
@media (min-width: 1024px) { .container { padding: var(--space-8); max-width: 1200px; margin: 0 auto; } }
Every async operation MUST show appropriate feedback.
// Loading
<button disabled={isLoading}>
{isLoading ? <><Spinner aria-hidden="true" /><span>Saving...</span></> : 'Save'}
</button>
<div aria-busy="true" aria-label="Loading content">
<div class="skeleton skeleton-text" />
</div>
// Error
<div role="alert" class="error-banner">
<ErrorIcon aria-hidden="true" />
<div>
<p class="error-message">Failed to save changes</p>
<p class="error-detail">Please check your connection and try again</p>
</div>
<button onClick={retry}>Retry</button>
</div>