Modern UX/UI: UX Laws, mobile-first, SaaS patterns, micro-interactions. Tailwind v3, shadcn, Motion, Recharts, TanStack.
Skill para criar interfaces modernas, distintivas e centradas no usuário para SaaS.
Use para: Components, pages, layouts, dashboards, charts, tables, forms Não use para: Backend (backend-development), Database (database-development)
Support Files:
.fnd/skills/ux-design/ux-laws-principles.md - UX Laws, Cognitive Load, Mental Models.fnd/skills/ux-design/modern-patterns.md - Interaction patterns, Visual trends, Performance UX.fnd/skills/ux-design/ux-writing.md - Microcopy, Error messages, Empty statesFull details:
ux-laws-principles.md
| Law | Rule | Application |
|---|---|---|
| Fitts | Larger + closer = easier |
| CTAs grandes, ações primárias acessíveis |
| Hick | More options = slower decision | Max 5-7 items visíveis, progressive disclosure |
| Miller | 7±2 chunks | Agrupar info em seções, não listas longas |
| Jakob | Users expect patterns | Não reinventar, seguir convenções |
| Doherty | <400ms = flow state | Feedback imediato, optimistic UI |
| Peak-End | Memory = peak + end | Celebrar sucesso, polish no final |
| Aesthetic-Usability | Beautiful = easier | Investir em visual polish |
{"intrinsic":"simplificar fluxo","extraneous":"eliminar ruído visual","germane":"patterns consistentes"}
Full details:
modern-patterns.md
// User clicks → UI updates instantly → Request fires → Rollback if error
const handleLike = async () => {
setLiked(true) // Optimistic
try { await api.like(id) }
catch { setLiked(false); toast.error("Failed") } // Rollback
}
{"when":"10+ actions available","must":"fuzzy search, recent items, keyboard nav","lib":"cmdk via shadcn"}
{"pattern":"click → input appears → blur/enter saves","feedback":"subtle border, auto-save indicator","when":"frequent single-field edits"}
{"pattern":"checkbox col → selection count → sticky action bar bottom","keyboard":"shift+click range, ctrl+click toggle"}
| Content Type | Recommendation |
|---|---|
| Feed/timeline | Infinite + virtualization |
| Search results | Pagination |
| Data tables | Pagination + page size selector |
| Gallery/cards | Load more button |
{"layout":"KPIs→Charts→Activity","kpis":{"grid":"grid-cols-2 md:grid-cols-4","card":"icon+value+label+trend","max":4},"charts":{"line":"trends","bar":"comparisons","h":"h-[200px] md:h-[300px]"},"activity":"avatar+action+timestamp","mobile":{"kpis":"2col,swipe>4","charts":"full-w,h-scroll"}}
{"layout":{"desktop":"sidebar→forms","mobile":"accordion|tabs"},"sections":["General","Profile","Notifications","Security","Billing","Team","API"],"forms":{"label":"above","save":"sticky-bottom-mobile"},"feedback":{"save":"toast 3s","unsaved":"warning dialog"},"danger":"red zone bottom+confirm"}
{"pricing":{"tiers":3,"highlight":"Popular badge+border-primary","toggle":"monthly/annual"},"cards":"name→price→features→CTA","usage":{"display":"Progress current/limit","warn":"yellow@80%,red@95%"},"invoices":{"cols":"date|desc|amount|status|actions","mobile":"cards"},"checkout":"plan→payment→confirm→success"}
{"flow":["Welcome","Profile","FirstAction","Success"],"maxSteps":5,"progress":"stepper|checklist","empty":"illustration+headline+desc+CTA","tooltips":{"max":3,"dismiss":"click|X"},"celebration":"confetti/animation on completion"}
{"layout":"filters→table→pagination","header":{"search":"debounce 300ms","filters":"dropdown+chips","bulk":"on selection"},"table":"checkbox|main|secondary|status|actions","mobile":"cards|h-scroll+sticky-col1","states":{"loading":"skeleton 3-5 rows","empty":"illust+CTA","error":"msg+retry"}}
{"login":"email+pwd,social,forgot,signup link","signup":"name+email+pwd,terms,social","layout":{"desktop":"split form|illustration","mobile":"centered,logo top"},"flows":{"magic":"email→link→inbox→logged","forgot":"email→reset→newpwd→success","2fa":"6digits+resend"}}
{"members":"avatar+name+email+role+actions","roles":["Owner","Admin","Member","Viewer"],"invite":"email+role+send,pending list","switcher":"header dropdown,current highlighted","settings":["General","Members","Billing","DangerZone"]}
{"desktop":{"sidebar":"logo→nav→spacer→user,collapsible 240px→60px","header":"breadcrumb|search|notif|user,h-14 md:h-16,sticky"},"mobile":{"bottomNav":"5 max,icon+label,h-16","drawer":"hamburger→full nav"},"states":{"active":"bg-muted+text-primary+font-medium"}}
{"layout":"max-w-2xl,space-y-6 sections,space-y-4 fields","fields":"label above+required*,placeholder=example,helper=muted,error=destructive","validation":"blur first,change after error,inline errors","actions":"bottom right,primary+secondary outline","mobile":"sticky bottom+safe-area","autosave":"draft indicator for long forms"}
{"sizes":{"sm":"max-w-sm","md":"max-w-md","lg":"max-w-lg","full":"max-w-4xl"},"structure":"header(title+X)→content(scroll)→footer(actions right)","mobile":"drawer bottom (Vaul)","behavior":"X|Esc|outside close,focus trap"}
{"toast":{"position":"bottom-right,mobile:bottom-center","success":"green,auto 3s","error":"red,manual+retry","warning":"yellow,manual"},"loading":{"content":"Skeleton","actions":"Spinner+disable","progress":"uploads"},"confirm":{"destructive":"AlertDialog red","standard":"Dialog"}}
Auto-detect SaaS context from keywords:
| Keywords | Context | Pattern |
|---|---|---|
| dashboard,metrics,KPIs,analytics | Dashboard | Dashboard |
| settings,preferences,config,profile | Settings | Settings |
| billing,pricing,plans,subscription | Billing | Billing |
| onboarding,welcome,setup,wizard | Onboarding | Onboarding |
| list,table,CRUD,manage | DataTables | DataTables |
| login,signup,auth,password | Auth | Auth |
| team,members,workspace,invite | Workspace | Workspace |
| notifications,alerts | Feedback | Feedback |
| form,input,create,edit | Forms | Forms |
| modal,dialog,popup,drawer | Modal | Modal |
Multiple contexts: "Team Settings" → Settings + Workspace
| Action | Response Time | Feedback Type |
|---|---|---|
| Click/tap | < 100ms | Visual change (scale, color) |
| Form submit | < 500ms show spinner | Disable button + spinner |
| Content load | > 300ms | Skeleton loader |
| Toast display | 3-5s auto-dismiss | Bottom-right (desktop) |
| Animation duration | 200-400ms | Ease-out for exits |
idle → hover(scale-[1.02]) → active(scale-[0.98]) → loading(spinner+disabled) → success(check)/error(shake) → idle
| Type | When to Use |
|---|---|
| Spinner | Unknown duration < 4s |
| Progress bar | Known steps/percentage |
| Skeleton | Content placeholder |
| Percentage text | File uploads, long processes |
{"subtle":"checkmark animation + green pulse","milestone":"confetti for achievements, first actions","rule":"match importance of action"}
Full details:
modern-patterns.md
{"when":"feature showcases, varied dashboards","pattern":"asymmetric grid-cols, varied item sizes","rule":"max 4 different sizes"}
<div className="bg-white/10 backdrop-blur-md border border-white/20 shadow-lg shadow-black/5">
// Use for: overlays, highlighted cards, hero elements
// Avoid: small text over glass, overuse
{"principle":"design dark first, derive light","benefits":"vibrant colors, less eye strain","must":"AA contrast ratio in dark mode"}
// WRONG: Pure black shadows
shadow-lg // can look dirty
// CORRECT: Tinted subtle shadows
className="shadow-lg shadow-primary/5"
// or custom with color tint
{"60%":"neutral (background)","30%":"secondary (cards/surfaces)","10%":"accent (CTAs/highlights)"}
{"display":["Clash Display","Cabinet Grotesk","Satoshi","Plus Jakarta Sans"],"body":["DM Sans","Inter (if must)","Source Sans Pro"],"mono":["JetBrains Mono","Fira Code"]}
// Headings
<h1 className="text-2xl md:text-3xl lg:text-4xl font-display font-bold">
<h2 className="text-xl md:text-2xl font-display font-semibold">
// Body
<p className="text-sm md:text-base text-muted-foreground">
// Small
<span className="text-xs text-muted-foreground">
// CORRECT: Mobile-first (320px base)
<div className="p-4 md:p-6 lg:p-8">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3">
// WRONG: Desktop-first
<div className="p-8 sm:p-4"> // NEVER
{"minTarget":"44x44px","spacing":"8px between touch elements","feedback":"active:scale-[0.98] or bg change"}
// ALWAYS 16px+ for inputs (prevents iOS zoom)
<Input className="text-base" /> // 16px minimum
// NEVER text-sm on mobile inputs
| Element | Desktop | Mobile |
|---|---|---|
| Modals | Centered dialog | Bottom drawer (Vaul) |
| Navigation | Sidebar | Bottom nav (5 max) or hamburger |
| Tables | Full table | Cards or horizontal scroll |
| Filters | Inline dropdowns | Drawer/expandable |
| Forms | Inline submit | Sticky bottom + safe-area |
<div className="fixed bottom-0 inset-x-0 p-4 pb-safe">
// CSS: padding-bottom: max(1rem, env(safe-area-inset-bottom));
Full details:
ux-writing.md
{"structure":"What happened + Why + How to fix","good":"Email already registered. Try logging in instead.","bad":"Error: duplicate key violation"}
{"structure":"Title + Value description + CTA","example":"No projects yet → Create your first project to start organizing work → Create Project"}
Every component/page must handle ALL states:
// Loading
if (isLoading) return <Skeleton className="h-[200px]" />
// Error
if (error) return (
<ErrorState
message="Failed to load data"
action={<Button onClick={refetch}>Try again</Button>}
/>
)
// Empty
if (!data?.length) return (
<EmptyState
icon={<FileIcon />}
title="No items yet"
description="Create your first item to get started"
action={<Button>Create Item</Button>}
/>
)
// Success
return <DataDisplay data={data} />
// Prefetch on hover
<Link prefetch onMouseEnter={() => prefetch(url)}>
// Preload images
<Image placeholder="blur" blurDataURL={tiny} />
| Pattern | Why Bad | Fix |
|---|---|---|
| Gradients on long text | Hurts readability | Short titles only |
| Pure black shadows | Look dirty | Tinted shadows (primary/5) |
| >3 vibrant colors | Distracts from content | 60-30-10 rule |
| Purple gradients on white | AI cliché | Subtle, token-based |
| Desktop-first breakpoints | Mobile afterthought | 320px base always |
| Centered modals on mobile | Bad touch UX | Vaul bottom drawers |
| Touch targets <44px | Frustrating | Min 44x44px |
| Inputs <16px font | iOS auto-zoom | text-base minimum |
| Generic loading text | Feels slow | Contextual messages |
| No empty states | Confusing | Always design empty |
prefers-reduced-motion support// Visible focus
className="focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
// Trap focus in modals (shadcn does this)
// Restore focus on close
// Respect user preference
className="motion-safe:animate-fadeIn"
// Or in CSS
@media (prefers-reduced-motion: reduce) {
* { animation-duration: 0.01ms !important; }
}
| Component | UX Rule | Implementation |
|---|---|---|
| Sidebar | Z-Pattern: Logo→Nav→Profile | bg-background + border 1px subtle |
| KPI Cards | Value is hero, label is support | font-display for number, text-muted for label |
| Charts | Less is more - no excessive grid | Subtle grid, bg-popover tooltip |
| Navigation | Active visible but not invasive | text-primary + bg-primary/10 |
| Tables | Data > decoration | Subtle borders, hover row, actions right |
| Forms | Clear labels, inline errors | text-destructive errors, text-muted helpers |
| Buttons | Primary action obvious | One primary per view, rest secondary/ghost |
| Empty States | Guide, don't abandon | Illustration + headline + CTA |
{"shadcn":".fnd/skills/ux-design/shadcn-docs.md"} {"tailwind":".fnd/skills/ux-design/tailwind-v3-docs.md"} {"motion":".fnd/skills/ux-design/motion-dev-docs.md"} {"recharts":".fnd/skills/ux-design/recharts-docs.md"} {"tanstackTable":".fnd/skills/ux-design/tanstack-table-docs.md"} {"tanstackQuery":".fnd/skills/ux-design/tanstack-query-docs.md"} {"tanstackRouter":".fnd/skills/ux-design/tanstack-router-docs.md"} {"uxLaws":".fnd/skills/ux-design/ux-laws-principles.md"} {"modernPatterns":".fnd/skills/ux-design/modern-patterns.md"} {"uxWriting":".fnd/skills/ux-design/ux-writing.md"}
{"core":[{"name":"shadcn/ui","for":"components"},{"name":"tailwindcss","for":"styling"},{"name":"motion","for":"animations"}]} {"data":[{"name":"recharts","for":"charts"},{"name":"@tanstack/react-table","for":"tables"},{"name":"@tanstack/react-query","for":"data fetching"}]} {"ux":[{"name":"sonner","for":"toasts","cmd":"npx shadcn add sonner"},{"name":"vaul","for":"mobile drawers","cmd":"npx shadcn add drawer"},{"name":"cmdk","for":"command palette","cmd":"npx shadcn add command"},{"name":"nuqs","for":"URL state"},{"name":"@tanstack/react-virtual","for":"1000+ items"}]}
<div className="min-h-screen bg-background">
<header className="sticky top-0 z-50 border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
<div className="container flex h-14 md:h-16 items-center px-4" />
</header>
<div className="container flex flex-col md:flex-row gap-6 p-4 md:p-6">
<aside className="hidden md:block w-64 shrink-0">
<nav className="sticky top-20 space-y-2" />
</aside>
<main className="flex-1 min-w-0 space-y-6" />
</div>
</div>
<Card className="group cursor-pointer transition-all hover:shadow-lg hover:shadow-primary/5 hover:border-primary/50">
<CardHeader>
<CardTitle className="group-hover:text-primary transition-colors">
{title}
</CardTitle>
</CardHeader>
</Card>
<motion.ul initial="hidden" animate="show" variants={{
hidden: { opacity: 0 },
show: { opacity: 1, transition: { staggerChildren: 0.05 } }
}}>
{items.map((item) => (
<motion.li key={item.id} variants={{
hidden: { opacity: 0, y: 10 },
show: { opacity: 1, y: 0 }
}}>
{item.name}
</motion.li>
))}
</motion.ul>
<div className="h-[200px] md:h-[300px] w-full">
<ResponsiveContainer width="100%" height="100%">
<LineChart data={data}>
<CartesianGrid strokeDasharray="3 3" className="stroke-muted" />
<XAxis dataKey="name" className="text-xs" tick={{ fill: 'hsl(var(--muted-foreground))' }} />
<Tooltip contentStyle={{ backgroundColor: 'hsl(var(--popover))', border: '1px solid hsl(var(--border))' }} />
<Line type="monotone" dataKey="value" stroke="hsl(var(--primary))" strokeWidth={2} dot={false} />
</LineChart>
</ResponsiveContainer>
</div>