| name | ux-principles |
| description | User experience design principles for developers |
| domain | software-design |
| version | 1.0.0 |
| tags | ["ux","usability","accessibility","responsive","design-system","wcag"] |
| triggers | {"keywords":{"primary":["ux","user experience","usability","accessibility","a11y","wcag"],"secondary":["responsive","design system","heuristics","user flow","information architecture"]},"context_boost":["frontend","ui","design","user","interface"],"context_penalty":["backend","database","devops"],"priority":"medium"} |
UX Principles
Overview
Essential UX principles that every developer should know. Good UX isn't just design—it's built into code, architecture, and technical decisions.
Nielsen's 10 Usability Heuristics
1. Visibility of System Status
async function saveDocument() {
await api.save(document);
}
async function saveDocument() {
setStatus('saving');
try {
await api.save(document);
setStatus('saved');
showToast('Document saved');
} catch (error) {
setStatus('error');
showToast('Failed to save. Please try again.');
}
}
<button disabled={isLoading}>
{isLoading ? (
<>
<Spinner /> Saving...
</>
) : (
'Save'
)}
</button>
<progress value={uploadProgress} max="100" />
<span>{uploadProgress}% uploaded</span>
2. Match Between System and Real World
"Error: ECONNREFUSED 127.0.0.1:5432"
"We couldn't connect to the database. Please check your internet connection."
"Record not found in users table"
"We couldn't find an account with that email address"
3. User Control and Freedom
function deleteItem(id: string) {
const item = items.find(i => i.id === id);
setItems(items.filter(i => i.id !== id));
showToast({
message: 'Item deleted',
action: {
label: 'Undo',
onClick: () => setItems([...items, item])
},
duration: 5000
});
}
const controller = new AbortController();
async function uploadFile(file: File) {
try {
await fetch('/upload', {
method: 'POST',
body: file,
signal: controller.signal
});
} catch (e) {
if (e.name === 'AbortError') {
showToast('Upload cancelled');
}
}
}
<button onClick={ controller.()}> </button>
4. Consistency and Standards
const theme = {
colors: {
primary: '#007bff',
danger: '#dc3545',
success: '#28a745',
},
spacing: {
xs: '4px',
sm: '8px',
md: '16px',
lg: '24px',
},
borderRadius: {
sm: '4px',
md: '8px',
lg: '16px',
}
};
<Button variant="primary">Save</Button>
<Button variant="secondary">Cancel</Button>
<Button variant="danger">Delete</Button>
5. Error Prevention
function deleteAccount() {
const confirmed = await confirm({
title: 'Delete Account?',
message: 'This action cannot be undone. All your data will be permanently deleted.',
confirmText: 'Delete Account',
confirmVariant: 'danger'
});
if (confirmed) {
await api.deleteAccount();
}
}
<input
type="number"
min={0}
max={100}
step={1}
inputMode="numeric"
/>
<button
disabled={!isFormValid || isSubmitting}
title={!isFormValid ? 'Please fill all required fields' : undefined}
>
Submit
</button>
Accessibility (WCAG)
Semantic HTML
<div class="nav">
<div class="nav-item" onclick="navigate()">Home</div>
</div>
<nav aria-label="Main navigation">
<ul>
<li><a href="/">Home</a></li>
</ul>
</nav>
<input type="text" placeholder="Email">
<label for="email">Email address</label>
<input type="email" id="email" name="email" required>
ARIA Attributes
<div aria-live="polite" aria-atomic="true">
{statusMessage}
</div>
<div
role="dialog"
aria-modal="true"
aria-labelledby="dialog-title"
aria-describedby="dialog-description"
>
<h2 id="dialog-title">Confirm Action</h2>
<p id="dialog-description">Are you sure you want to proceed?</p>
</div>
<button aria-busy={isLoading} aria-disabled={isLoading}>
{isLoading ? 'Loading...' : 'Submit'}
</button>
<button
aria-expanded={isOpen}
aria-controls="panel-content"
>
Show Details
</button>
<div id="panel-content" hidden={!isOpen}>
Details here...
Keyboard Navigation
function openModal() {
setIsOpen(true);
setTimeout(() => {
modalRef.current?.querySelector('button, [href], input')?.focus();
}, 0);
}
function closeModal() {
setIsOpen(false);
triggerRef.current?.focus();
}
function handleKeyDown(e: KeyboardEvent) {
if (e.key === 'Tab') {
const focusable = modalRef.current?.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
const first = focusable?.[0];
const last = focusable?.[focusable.length - 1];
if (e.shiftKey && document.activeElement === first) {
e.preventDefault();
last?.focus();
} else if (!e.shiftKey && . === last) {
e.();
first?.();
}
}
(e. === ) {
();
}
}
Color and Contrast
:root {
--text-primary: #1a1a1a;
--text-secondary: #6b7280;
--text-on-primary: #ffffff;
}
.error-message {
color: #dc3545;
&::before {
content: "⚠ ";
}
}
:focus-visible {
outline: 2px solid var(--focus-color);
outline-offset: 2px;
}
:focus { outline: none; }
Responsive Design
Mobile-First Approach
.container {
padding: 16px;
}
.grid {
display: grid;
gap: 16px;
grid-template-columns: 1fr;
}
@media (min-width: 768px) {
.container {
padding: 24px;
}
.grid {
grid-template-columns: repeat(2, 1fr);
}
}
@media (min-width: 1024px) {
.container {
padding: 32px;
max-width: 1200px;
margin: 0 auto;
}
.grid {
grid-template-columns: repeat(3, 1fr);
}
}
Touch Targets
.button {
min-height: 44px;
min-width: 44px;
padding: 12px 16px;
}
.button-group {
display: flex;
gap: 8px;
}
.card-link {
position: relative;
}
.card-link::after {
content: '';
position: absolute;
inset: 0;
}
Performance as UX
Perceived Performance
function likePost(postId: string) {
setLiked(true);
setLikeCount(prev => prev + 1);
api.likePost(postId).catch(() => {
setLiked(false);
setLikeCount(prev => prev - 1);
showToast('Failed to like post');
});
}
function PostList() {
if (isLoading) {
return (
<div className="post-list">
{[1, 2, 3].map(i => (
<div key={i} className="post-skeleton">
<div className="skeleton-avatar" />
<div className="skeleton-text" />
<div className= />
))}
);
}
;
}
Content Prioritization
<head>
<link rel="preload" href="/fonts/main.woff2" as="font" crossorigin>
<link rel="preload" href="/hero-image.webp" as="image">
<link rel="preload" href="/non-critical.css" as="style" onload="this.rel='stylesheet'">
</head>
<img src="product.jpg" loading="lazy" alt="Product image">
<div ref={sentinelRef}>
{hasMore && <Spinner />}
</div>
Forms UX
Input Design
<div class="form-field">
<label for="password">Password</label>
<input
type="password"
id="password"
aria-describedby="password-help"
minlength="8"
>
<small id="password-help">At least 8 characters</small>
</div>
<input
type="email"
class={hasError ? 'input-error' : ''}
aria-invalid={hasError}
aria-describedby={hasError ? 'email-error' : undefined}
>
{hasError && (
<span id="email-error" class="error-message" role="alert">
Please enter a valid email address
</span>
)}
Form Patterns
const debouncedSave = useMemo(
() => debounce((data) => saveDraft(data), 1000),
[]
);
useEffect(() => {
debouncedSave(formData);
}, [formData]);
function handleChange(field: string, value: string) {
setFormData(prev => ({ ...prev, [field]: value }));
setErrors(prev => ({ ...prev, [field]: undefined }));
}
useBeforeUnload(
useCallback((e) => {
if (hasUnsavedChanges) {
e.preventDefault();
return 'You have unsaved changes';
}
}, [hasUnsavedChanges])
);
Empty States
function EmptyState({ type }: { type: 'search' | 'empty' | 'error' }) {
const content = {
search: {
icon: <SearchIcon />,
title: 'No results found',
message: 'Try adjusting your search or filters',
action: <Button onClick={clearFilters}>Clear filters</Button>
},
empty: {
icon: <FolderIcon />,
title: 'No projects yet',
message: 'Create your first project to get started',
action: <Button onClick={createProject}>Create Project</Button>
},
error: {
icon: <AlertIcon />,
title: 'Something went wrong',
message: 'We couldn\'t load the data. Please try again.',
action: <Button onClick=>Retry
}
}[];
(
);
}
Related Skills
- [[frontend]] - UI implementation
- [[design-patterns]] - UI patterns
- [[accessibility]] - Detailed WCAG compliance