---
name: epic-ui-guidelines
description:
Guide on UI/UX guidelines, accessibility, and component usage for Epic Stack
categories:
- ui
- accessibility
- tailwind
- radix-ui
---
# Epic Stack: UI Guidelines
## When to use this skill
Use this skill when you need to:
- Create accessible UI components
- Follow Epic Stack design patterns
- Use Tailwind CSS effectively
- Implement semantic HTML
- Add ARIA attributes correctly
- Create responsive layouts
- Ensure proper form accessibility
- Follow Epic Stack's UI component conventions
## Patterns and conventions
### UI Philosophy
Following Epic Web principles:
**Software is built for people, by people** - Accessibility isn't about checking
boxes or meeting standards. It's about creating software that works for real
people with diverse needs, abilities, and contexts. Every UI decision should
prioritize the human experience over technical convenience.
Accessibility is not optional - it's how we ensure our software serves all
users, not just some. When you make UI accessible, you're making it better for
everyone: clearer labels help all users, keyboard navigation helps power users,
and semantic HTML helps search engines.
**Example - Human-centered approach:**
```typescript
// ✅ Good - Built for people
function NoteForm() {
return (
)
}
// ❌ Avoid - Technical convenience over user experience
function NoteForm() {
return (
)
}
```
### Semantic HTML
**✅ Good - Use semantic elements:**
```typescript
function UserCard({ user }: { user: User }) {
return (
{user.name}
{user.bio}
)
}
```
**❌ Avoid - Generic divs:**
```typescript
// ❌ Don't use divs for everything
{user.name}
{user.bio}
{formatDate(user.createdAt)}
```
### Form Accessibility
**✅ Good - Always use labels:**
```typescript
import { Field } from '#app/components/forms.tsx'
```
The `Field` component automatically:
- Associates labels with inputs using `htmlFor` and `id`
- Adds `aria-invalid` when there are errors
- Adds `aria-describedby` pointing to error messages
- Ensures proper error announcement
**❌ Avoid - Unlabeled inputs:**
```typescript
// ❌ Don't forget labels
```
### ARIA Attributes
**✅ Good - Use ARIA appropriately:**
```typescript
// Epic Stack's Field component handles this automatically
```
**✅ Good - ARIA for custom components:**
```typescript
function LoadingButton({ isLoading, children }: { isLoading: boolean; children: React.ReactNode }) {
return (
)
}
```
### Using Radix UI Components
Epic Stack uses Radix UI for accessible, unstyled components.
**✅ Good - Use Radix primitives:**
```typescript
import * as Dialog from '@radix-ui/react-dialog'
import { Button } from '#app/components/ui/button.tsx'
function MyDialog() {
return (
Dialog TitleDialog description
)
}
```
Radix components automatically handle:
- Keyboard navigation
- Focus management
- ARIA attributes
- Screen reader announcements
### Tailwind CSS Patterns
**✅ Good - Use Tailwind utility classes:**
```typescript
function Card({ children }: { children: React.ReactNode }) {
return (
{children}
)
}
```
**✅ Good - Use Tailwind responsive utilities:**
```typescript
{items.map(item => (
{item.name}
))}
```
**✅ Good - Use Tailwind dark mode:**
```typescript
{content}
```
### Error Handling in Forms
**✅ Good - Display errors accessibly:**
```typescript
import { Field, ErrorList } from '#app/components/forms.tsx'
// Form-level errors
```
Errors are automatically:
- Associated with inputs via `aria-describedby`
- Announced to screen readers
- Visually distinct with error styling
### Focus Management
**✅ Good - Visible focus indicators:**
```typescript
// Tailwind's default focus:ring handles this
```
**✅ Good - Focus on form errors:**
```typescript
import { useEffect, useRef } from 'react'
function FormWithErrorFocus() {
const firstErrorRef = useRef(null)
useEffect(() => {
if (actionData?.errors && firstErrorRef.current) {
firstErrorRef.current.focus()
}
}, [actionData?.errors])
return
}
```
### Keyboard Navigation
**✅ Good - Keyboard accessible components:**
```typescript
// Radix components handle keyboard navigation automatically
// Custom components should support keyboard
```
### Color Contrast
**✅ Good - Use accessible color combinations:**
```typescript
// Use Tailwind's semantic colors that meet WCAG AA
// Very low contrast
```
### Responsive Design
**✅ Good - Mobile-first approach:**
```typescript
{/* Content */}
```
**✅ Good - Responsive typography:**
```typescript
Responsive Heading
```
### Loading States
**✅ Good - Accessible loading indicators:**
```typescript
import { useNavigation } from 'react-router'
function SubmitButton() {
const navigation = useNavigation()
const isSubmitting = navigation.state === 'submitting'
return (
)
}
```
### Icon Usage
**✅ Good - Decorative icons:**
```typescript
import { Icon } from '#app/components/ui/icon.tsx'
```
**✅ Good - Semantic icons:**
```typescript
```
### Skip Links
**✅ Good - Add skip to main content:**
```typescript
// In your root layout
Skip to main content
{/* Main content */}
```
### Progressive Enhancement
**✅ Good - Forms work without JavaScript:**
```typescript
// Conform forms work without JavaScript
```
Forms automatically:
- Submit via native HTML forms if JavaScript is disabled
- Validate server-side
- Show errors appropriately
### Screen Reader Best Practices
**✅ Good - Use semantic HTML first:**
```typescript
// ✅ Semantic HTML provides context automatically
```
**✅ Good - Announce dynamic content:**
```typescript
import { useNavigation } from 'react-router'
function SearchResults({ results }: { results: Result[] }) {
const navigation = useNavigation()
const isSearching = navigation.state === 'loading'
return (
)
}
```
**✅ Good - Live regions for important updates:**
```typescript
function ToastContainer({ toasts }: { toasts: Toast[] }) {
return (
{toasts.map(toast => (
{toast.message}
))}
)
}
```
**ARIA live region options:**
- `aria-live="polite"` - For non-critical updates (search results, status
messages)
- `aria-live="assertive"` - For critical updates (errors, confirmations)
- `aria-atomic="true"` - Screen reader reads entire region on update
- `aria-atomic="false"` - Screen reader reads only changed parts
### Keyboard Navigation Patterns
**✅ Good - Tab order follows visual order:**
```typescript
// Elements appear in logical tab order
```
**✅ Good - Keyboard shortcuts:**
```typescript
import { useEffect } from 'react'
function SearchDialog({ onClose }: { onClose: () => void }) {
useEffect(() => {
function handleKeyDown(e: KeyboardEvent) {
if (e.key === 'Escape') {
onClose()
}
}
window.addEventListener('keydown', handleKeyDown)
return () => window.removeEventListener('keydown', handleKeyDown)
}, [onClose])
return
}
```
**✅ Good - Focus trap in modals:**
```typescript
// Radix Dialog automatically handles focus trap
{/* Focus is trapped inside dialog */}
Close
```
### Focus Management for React Router
**✅ Good - Focus on route changes:**
```typescript
import { useEffect } from 'react'
import { useNavigation } from 'react-router'
function RouteComponent() {
const navigation = useNavigation()
const mainRef = useRef(null)
useEffect(() => {
if (navigation.state === 'idle' && mainRef.current) {
mainRef.current.focus()
}
}, [navigation.state])
return (
{/* Content */}
)
}
```
**✅ Good - Focus on errors:**
```typescript
import { useEffect, useRef } from 'react'
function FormWithErrorFocus({ actionData }: Route.ComponentProps) {
const firstErrorRef = useRef(null)
useEffect(() => {
if (actionData?.errors && firstErrorRef.current) {
// Focus first error field
firstErrorRef.current.focus()
// Announce error
firstErrorRef.current.setAttribute('aria-invalid', 'true')
}
}, [actionData?.errors])
return
}
```
### Typography and Readability
**✅ Good - Readable text sizes:**
```typescript
// Use Tailwind's text size scale
Readable body text
Clear headings
```
**✅ Good - Sufficient line height:**
```typescript
// Tailwind defaults provide good line height
Comfortable reading
```
**❌ Avoid - Small or hard-to-read text:**
```typescript
// ❌ Don't use very small text
Hard to read
```
### Touch Target Sizes
**✅ Good - Sufficient touch targets:**
```typescript
// Buttons should be at least 44x44px (touch target size)
```
**✅ Good - Spacing between interactive elements:**
```typescript
```
### Internationalization (i18n) Considerations
**✅ Good - Use semantic HTML for dates/times:**
```typescript
```
**✅ Good - Use semantic HTML for numbers:**
```typescript
// Screen readers can pronounce numbers correctly
Total: {count}
```
**✅ Good - Language attributes:**
```typescript
// In root.tsx
{/* Content */}
```
### Dark Mode Accessibility
**✅ Good - Maintain contrast in dark mode:**
```typescript
// Ensure sufficient contrast in both modes
{content}
```
**✅ Good - Respect user preference:**
```typescript
// Epic Stack automatically handles theme preference
// Use semantic colors that work in both modes
```
### Animation and Motion
**✅ Good - Respect reduced motion:**
```typescript
// Tailwind automatically respects prefers-reduced-motion
{/* Animations disabled for users who prefer reduced motion */}
```
**✅ Good - Use CSS for animations:**
```typescript
// ✅ CSS animations can be disabled via prefers-reduced-motion
{/* Content */}
// ❌ JavaScript animations may not respect user preferences
```
## Common mistakes to avoid
- ❌ **Treating accessibility as a checklist**: Accessibility is about serving
real people, not just meeting standards
- ❌ **Missing form labels**: Always use `Field` component which includes
labels - helps all users, not just screen reader users
- ❌ **Using divs for semantic elements**: Use ``, ``, `