--- 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 (
{/* No label, no guidance, no accessibility */}
) } ``` ### 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 Title Dialog 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
// High contrast
// Accessible links ``` **❌ Avoid - Low contrast text:** ```typescript // ❌ Don't use low contrast
// 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
Submit ``` 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 (
{isSearching ? 'Searching...' : `${results.length} results found`}
) } ``` **✅ 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 {/* content */} } ``` **✅ 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 `
`, `
`, `