Loading...
Loading...
UI/UX best practices and accessibility guidelines. Use when reviewing UI code, checking accessibility, auditing forms, or ensuring web interface best practices. Triggers on "review UI", "check accessibility", "audit design", "review UX", or "check best practices".
npx skill4agent add asyrafhussin/agent-skills web-design-guidelines| Priority | Category | Impact | Prefix |
|---|---|---|---|
| 1 | Accessibility - Semantic Structure | CRITICAL | |
| 2 | Accessibility - Keyboard & Focus | CRITICAL | |
| 3 | Accessibility - Visual & Color | CRITICAL | |
| 4 | Forms - Input & Validation | CRITICAL | |
| 5 | Forms - Error Handling | HIGH | |
| 6 | Forms - User Experience | MEDIUM | |
| 7 | Animation & Motion | CRITICAL | |
| 8 | Performance & UX | MEDIUM | |
a11y-semantic-htmla11y-heading-hierarchya11y-screen-readera11y-skip-linksa11y-keyboard-nava11y-focus-managementa11y-aria-labelsa11y-color-contrasta11y-alt-textform-autocompleteform-input-typesform-labelsform-error-displayform-error-messagesform-validation-uxform-inline-validationform-multi-stepform-placeholder-usageform-submit-feedbackmotion-reduced// ❌ Div soup - no semantic meaning
<div className="header">
<div className="nav">
<div onClick={handleClick}>Home</div>
</div>
</div>
<div className="content">
<div className="title">Page Title</div>
<div className="text">Content here...</div>
</div>
// ✅ Semantic HTML - accessible and meaningful
<header>
<nav aria-label="Main navigation">
<a href="/">Home</a>
</nav>
</header>
<main>
<article>
<h1>Page Title</h1>
<p>Content here...</p>
</article>
</main>// Interactive elements need accessible names
<button aria-label="Close dialog">
<XIcon />
</button>
<button aria-label="Add to cart">
<PlusIcon />
</button>
// Icon-only links
<a href="/settings" aria-label="Settings">
<SettingsIcon />
</a>
// Decorative icons should be hidden
<span aria-hidden="true">🎉</span>// All interactive elements must be keyboard accessible
function Dialog({ isOpen, onClose, children }) {
// Trap focus inside dialog
const dialogRef = useRef<HTMLDivElement>(null)
useEffect(() => {
if (isOpen) {
dialogRef.current?.focus()
}
}, [isOpen])
// Handle Escape key
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Escape') {
onClose()
}
}
return (
<div
ref={dialogRef}
role="dialog"
aria-modal="true"
tabIndex={-1}
onKeyDown={handleKeyDown}
>
{children}
<button onClick={onClose}>Close</button>
</div>
)
}// ✅ Always visible focus styles
<button className="
focus:outline-none
focus-visible:ring-2
focus-visible:ring-blue-500
focus-visible:ring-offset-2
">
Button
</button>
// ❌ Never remove focus outlines without replacement
<button className="outline-none focus:outline-none">
Inaccessible
</button>// ✅ Properly labeled form
<form>
<div>
<label htmlFor="email">
Email address
<span aria-hidden="true" className="text-red-500">*</span>
</label>
<input
id="email"
type="email"
name="email"
required
aria-required="true"
aria-describedby="email-hint email-error"
autoComplete="email"
/>
<p id="email-hint" className="text-gray-500 text-sm">
We'll never share your email.
</p>
{error && (
<p id="email-error" role="alert" className="text-red-500 text-sm">
{error}
</p>
)}
</div>
<button type="submit">Subscribe</button>
</form>// CSS
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
// Tailwind
<div className="
transition-transform duration-300
hover:scale-105
motion-reduce:transition-none
motion-reduce:hover:transform-none
">
Card
</div>
// JavaScript
const prefersReducedMotion = window.matchMedia(
'(prefers-reduced-motion: reduce)'
).matches
function animate() {
if (prefersReducedMotion) {
// Skip or simplify animation
return
}
// Full animation
}// ✅ Proper image implementation
<img
src="/hero.webp"
alt="Team collaborating around a whiteboard"
width={1200}
height={600}
loading="lazy"
decoding="async"
/>
// ✅ Decorative images
<img src="/pattern.svg" alt="" aria-hidden="true" />
// ✅ Responsive images
<picture>
<source
srcSet="/hero-mobile.webp"
media="(max-width: 768px)"
type="image/webp"
/>
<source srcSet="/hero.webp" type="image/webp" />
<img src="/hero.jpg" alt="Hero description" width={1200} height={600} />
</picture>// ✅ Minimum 44x44px touch targets
<button className="min-h-[44px] min-w-[44px] p-2">
<Icon className="w-6 h-6" />
</button>
// ✅ Adequate spacing between touch targets
<nav className="flex gap-2">
<a href="/" className="p-3">Home</a>
<a href="/about" className="p-3">About</a>
</nav>/* WCAG AA requires 4.5:1 for normal text, 3:1 for large text */
/* ❌ Insufficient contrast */
.low-contrast {
color: #999999; /* Gray on white: ~2.8:1 */
background: white;
}
/* ✅ Sufficient contrast */
.good-contrast {
color: #595959; /* Darker gray: ~7:1 */
background: white;
}
/* ✅ Don't rely on color alone */
.error {
color: #dc2626;
border-left: 4px solid #dc2626; /* Visual indicator */
}// Announce dynamic content to screen readers
<div
role="status"
aria-live="polite"
aria-atomic="true"
className="sr-only"
>
{message}
</div>
// Toast notifications
function Toast({ message }: { message: string }) {
return (
<div
role="alert"
aria-live="assertive"
className="fixed bottom-4 right-4 bg-gray-900 text-white p-4 rounded"
>
{message}
</div>
)
}file:line - [category] Description of issuesrc/components/Button.tsx:15 - [a11y] Missing aria-label on icon-only button
src/pages/Home.tsx:42 - [perf] Image missing width/height attributes
src/components/Form.tsx:28 - [form] Input missing associated labelrules/a11y-semantic-html.md
rules/form-autocomplete.md
rules/motion-reduced.md
rules/_sections.mdAGENTS.md