askill
frontend-ui-ux

frontend-ui-uxSafety 95Repository

Build user-centered, mobile-first, accessible, and performant interfaces with modern frontend practices. Use when designing components, writing CSS, optimizing Core Web Vitals, implementing responsive layouts, auditing accessibility, or when the UI feels wrong but you cannot pinpoint why.

6 stars
1.2k downloads
Updated 2/18/2026

Package Files

Loading files...
SKILL.md

Frontend UI/UX Expert

Core Principle

Design for the smallest screen first, the weakest network second, and the most constrained user always. Every pixel must earn its place.

Mobile-First Design

Mobile-first is not about shrinking a desktop layout. It is about starting with the essential content and progressively enhancing for larger viewports.

Why Mobile-First Matters

  • Over 60% of web traffic is mobile. Design for the majority first.
  • Constraints force clarity. If it works on a 320px screen, it works everywhere.
  • Performance is non-negotiable on 3G connections and budget devices.

Responsive Breakpoints

Start with no media query (mobile), then layer upward.

/* Base: mobile (no media query needed) */
.container {
  padding: 1rem;
  display: flex;
  flex-direction: column;
  gap: 1rem;
}

/* Tablet */
@media (min-width: 768px) {
  .container {
    flex-direction: row;
    padding: 2rem;
  }
}

/* Desktop */
@media (min-width: 1024px) {
  .container {
    max-width: 1200px;
    margin-inline: auto;
  }
}

/* Large desktop */
@media (min-width: 1440px) {
  .container {
    max-width: 1400px;
  }
}

Tailwind CSS 4 equivalent:

<div class="flex flex-col gap-4 p-4 md:flex-row md:p-8 lg:mx-auto lg:max-w-5xl xl:max-w-7xl">
  <!-- content -->
</div>

Touch Targets

  • Minimum 44x44px for all interactive elements (WCAG 2.5.8).
  • Add padding, not just width/height -- the tap area must be real.
  • Space interactive elements at least 8px apart to prevent mis-taps.
.btn-touch {
  min-height: 44px;
  min-width: 44px;
  padding: 12px 24px;
}

/* Increase tap area without increasing visual size */
.icon-button {
  position: relative;
}
.icon-button::after {
  content: "";
  position: absolute;
  inset: -8px;
}

Viewport Considerations

<meta name="viewport" content="width=device-width, initial-scale=1" />
  • Never use maximum-scale=1 or user-scalable=no -- these block pinch-to-zoom and violate WCAG.
  • Use dvh (dynamic viewport height) instead of vh to handle mobile browser chrome correctly.
.full-screen {
  min-height: 100dvh;
}

Component Architecture

Design System Thinking

A design system is not a component library. It is a shared language between design and engineering.

LayerContainsExample
TokensColors, spacing, typography, radii, shadows--color-primary-600, --space-4
PrimitivesAtoms with no business logic<Button>, <Input>, <Badge>
CompositesMolecules combining primitives<SearchBar>, <UserCard>
FeaturesOrganisms with business logic<JobApplicationForm>, <DashboardMetrics>
LayoutsPage shells and navigation<AppShell>, <SidebarLayout>

Atomic Design in React

// Primitive: Button (no business logic, fully controlled via props)
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
  variant: "primary" | "secondary" | "ghost";
  size: "sm" | "md" | "lg";
  loading?: boolean;
}

function Button({ variant, size, loading, children, ...props }: ButtonProps) {
  return (
    <button
      className={cn(
        "inline-flex items-center justify-center rounded-lg font-medium transition-colors",
        "focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-600",
        variants[variant],
        sizes[size],
        loading && "pointer-events-none opacity-60"
      )}
      disabled={loading || props.disabled}
      aria-busy={loading}
      {...props}
    >
      {loading && <Spinner className="mr-2 h-4 w-4" aria-hidden="true" />}
      {children}
    </button>
  );
}

Props and State Management for UI

ConcernWhere It LivesWhy
Visual state (open, hovered, focused)Component-local useStateNo other component needs it
Form field valuesForm library (react-hook-form) or parent stateControlled inputs need a single source of truth
Server data (user profile, job listings)Server state (React Query, SWR, or RSC)Cache invalidation, background refetch
Global UI state (theme, sidebar open)Context or lightweight store (Zustand)Shared across distant components
URL-driven state (filters, pagination)URL search paramsShareable, bookmarkable, survives refresh

Rule of thumb: if the state belongs in the URL, put it in the URL. If it comes from the server, let a cache manage it. Local useState is the last resort, not the first.

CSS Architecture

Modern CSS Primitives

Use flexbox for one-dimensional layouts, grid for two-dimensional layouts, and container queries when the component's size matters more than the viewport's size.

/* Flexbox: navigation bar */
.nav {
  display: flex;
  align-items: center;
  gap: 1rem;
}

/* Grid: card grid that auto-fills based on available space */
.card-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(100%, 300px), 1fr));
  gap: 1.5rem;
}

/* Container queries: component responds to its own container, not the viewport */
.card-container {
  container-type: inline-size;
}

@container (min-width: 400px) {
  .card {
    flex-direction: row;
  }
}

Tailwind CSS 4 Patterns

<!-- Responsive card with dark mode -->
<article class="rounded-xl bg-white p-4 shadow-sm ring-1 ring-gray-100
                dark:bg-gray-900 dark:ring-gray-800
                md:p-6">
  <h2 class="text-lg font-semibold text-gray-900 dark:text-gray-100">
    Job Title
  </h2>
  <p class="mt-1 text-sm text-gray-600 dark:text-gray-400">
    Company Name
  </p>
</article>

Dark Mode

Respect the user's OS preference by default. Provide a toggle that overrides it.

:root {
  --bg: #ffffff;
  --text: #111827;
}

@media (prefers-color-scheme: dark) {
  :root {
    --bg: #0f172a;
    --text: #f1f5f9;
  }
}

/* Override class applied via JS toggle */
[data-theme="dark"] {
  --bg: #0f172a;
  --text: #f1f5f9;
}

Responsive Typography

Use clamp() for fluid type that scales between breakpoints without media queries.

h1 {
  font-size: clamp(1.75rem, 1.25rem + 2vw, 3rem);
  line-height: 1.2;
}

body {
  font-size: clamp(1rem, 0.95rem + 0.25vw, 1.125rem);
  line-height: 1.6;
}

Accessibility (a11y)

Accessibility is not a feature. It is a baseline. Target WCAG 2.1 AA as a minimum.

Semantic HTML First

<!-- Bad: div soup -->
<div class="header">
  <div class="nav">
    <div class="link" onclick="navigate()">Home</div>
  </div>
</div>

<!-- Good: semantic structure -->
<header>
  <nav aria-label="Main navigation">
    <a href="/">Home</a>
  </nav>
</header>

Semantic elements provide built-in keyboard navigation, screen reader announcements, and focus management -- for free.

ARIA: Use Only When HTML Falls Short

// Custom dropdown needs ARIA because there is no native equivalent
<div role="listbox" aria-label="Select job category" aria-activedescendant={activeId}>
  {options.map((opt) => (
    <div
      key={opt.id}
      id={opt.id}
      role="option"
      aria-selected={opt.id === selectedId}
    >
      {opt.label}
    </div>
  ))}
</div>

The first rule of ARIA: do not use ARIA if a native HTML element does the job.

Keyboard Navigation Checklist

  • All interactive elements reachable with Tab
  • Focus order follows visual order (no positive tabindex values)
  • Escape closes modals, dropdowns, and overlays
  • Arrow keys navigate within composite widgets (tabs, menus, listboxes)
  • Focus is trapped inside open modals
  • Focus returns to the trigger element when a modal closes

Screen Reader Testing

Test with real screen readers, not just automated tools.

OSScreen ReaderBrowser
macOSVoiceOverSafari
WindowsNVDA (free)Firefox or Chrome
WindowsJAWSChrome
iOSVoiceOverSafari
AndroidTalkBackChrome

Color Contrast

  • Normal text: minimum 4.5:1 contrast ratio against background.
  • Large text (18px bold or 24px regular): minimum 3:1.
  • UI components and graphical objects: minimum 3:1.
  • Never convey information through color alone -- pair with icons, patterns, or text.

Performance

Core Web Vitals Targets

MetricWhat It MeasuresGoodNeeds WorkPoor
LCP (Largest Contentful Paint)Loading speed< 2.5s2.5-4s> 4s
INP (Interaction to Next Paint)Responsiveness< 200ms200-500ms> 500ms
CLS (Cumulative Layout Shift)Visual stability< 0.10.1-0.25> 0.25

Image Optimization

// Next.js Image component handles format, sizing, and lazy loading
import Image from "next/image";

<Image
  src="/hero.jpg"
  alt="AI developer working on a laptop"
  width={1200}
  height={630}
  priority                    // above-the-fold: skip lazy loading
  sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 1200px"
/>
  • Use WebP or AVIF formats -- 25-50% smaller than JPEG at equivalent quality.
  • Always set explicit width and height (or aspect-ratio in CSS) to prevent CLS.
  • Use loading="lazy" for below-the-fold images. Use fetchpriority="high" for hero images.

Lazy Loading and Code Splitting

// Route-level code splitting in Next.js App Router happens automatically.
// For component-level splitting:
import dynamic from "next/dynamic";

const HeavyChart = dynamic(() => import("@/components/HeavyChart"), {
  loading: () => <ChartSkeleton />,
  ssr: false,
});
// React Server Components (RSC): heavy data fetching stays on the server
// app/jobs/page.tsx (Server Component by default in Next.js App Router)
export default async function JobsPage() {
  const jobs = await getJobs();       // runs on server, zero client JS
  return <JobList jobs={jobs} />;     // JobList can be a client component for interactivity
}

Font Loading Strategy

Fonts are a top cause of layout shift and invisible text.

/* Preload the critical font */
/* In <head>: <link rel="preload" href="/fonts/Inter.woff2" as="font" type="font/woff2" crossorigin /> */

@font-face {
  font-family: "Inter";
  src: url("/fonts/Inter.woff2") format("woff2");
  font-display: swap;          /* show fallback text immediately, swap when loaded */
  font-weight: 100 900;
  unicode-range: U+0000-00FF;  /* subset to Latin characters if appropriate */
}
  • Use font-display: swap to prevent Flash of Invisible Text (FOIT).
  • Subset fonts to only the character ranges you need.
  • Self-host fonts instead of loading from Google Fonts to avoid extra DNS lookups.

UX Patterns for Common Flows

Forms

// react-hook-form with field-level validation and accessible errors
function ApplyForm() {
  const { register, handleSubmit, formState: { errors } } = useForm<ApplyFormData>();

  return (
    <form onSubmit={handleSubmit(onSubmit)} noValidate>
      <div>
        <label htmlFor="name">Full Name</label>
        <input
          id="name"
          aria-describedby={errors.name ? "name-error" : undefined}
          aria-invalid={!!errors.name}
          {...register("name", { required: "Name is required" })}
        />
        {errors.name && (
          <p id="name-error" role="alert" className="text-sm text-red-600">
            {errors.name.message}
          </p>
        )}
      </div>
      <button type="submit">Apply</button>
    </form>
  );
}

Rules:

  • Label every input. Placeholder text is not a label.
  • Show validation errors inline, next to the field.
  • Use aria-invalid and aria-describedby to connect errors to inputs.
  • Disable submit buttons only while submitting -- not while the form is invalid.

Navigation

  • Sticky or fixed header on mobile -- but keep it thin (max 56px).
  • Bottom navigation bar for primary actions on mobile apps.
  • Breadcrumbs on desktop for deep content hierarchies.
  • Always indicate the current page in navigation with aria-current="page".

Modals and Dialogs

// Use the native <dialog> element when possible
function ConfirmDialog({ open, onClose, onConfirm, title, message }: Props) {
  const ref = useRef<HTMLDialogElement>(null);

  useEffect(() => {
    if (open) ref.current?.showModal();
    else ref.current?.close();
  }, [open]);

  return (
    <dialog ref={ref} onClose={onClose} className="rounded-xl p-6 backdrop:bg-black/50">
      <h2 className="text-lg font-semibold">{title}</h2>
      <p className="mt-2 text-gray-600">{message}</p>
      <div className="mt-4 flex justify-end gap-3">
        <button onClick={onClose}>Cancel</button>
        <button onClick={onConfirm} className="bg-primary-600 text-white rounded-lg px-4 py-2">
          Confirm
        </button>
      </div>
    </dialog>
  );
}

Rules:

  • Trap focus inside the modal.
  • Close on Escape key press.
  • Return focus to the trigger element when the modal closes.
  • Prevent background scroll while the modal is open.
  • The native <dialog> element handles most of this automatically.

Loading, Empty, and Error States

Every data-fetching view needs three states beyond the happy path:

StateUX PatternImplementation
LoadingSkeleton screens (not spinners) -- they reduce perceived load timeRender placeholder shapes matching the layout
EmptyHelpful message + primary action ("No jobs saved yet. Browse open roles.")Check data.length === 0 after loading completes
ErrorClear message + retry action ("Something went wrong. Try again.")Catch at the boundary, show inline or toast
function JobList({ jobs, isLoading, error }: Props) {
  if (isLoading) return <JobListSkeleton count={6} />;
  if (error) return <ErrorState message="Failed to load jobs" onRetry={refetch} />;
  if (jobs.length === 0) return <EmptyState icon={<BriefcaseIcon />} message="No jobs found" action={<Link href="/explore">Explore roles</Link>} />;
  return <ul>{jobs.map(job => <JobCard key={job.id} job={job} />)}</ul>;
}

Toasts and Notifications

  • Auto-dismiss after 5 seconds for informational messages.
  • Persist until dismissed for errors and actions.
  • Use role="status" for informational toasts and role="alert" for errors.
  • Position at the bottom on mobile, top-right on desktop.
  • Stack multiple toasts vertically without overlapping content.

Infinite Scroll vs Pagination

FactorInfinite ScrollPagination
Content discoveryGood for feeds and social contentGood for search results and catalogs
Back button behaviorLoses scroll position without extra workNaturally bookmarkable
AccessibilityHarder -- announce new content to screen readersSimpler -- standard navigation
PerformanceCan degrade with thousands of DOM nodesConstant DOM size
SEORequires special handlingEach page is independently indexable

Recommendation: Use pagination for search results and job listings. Use infinite scroll only for social feeds, and virtualize the list with a library like @tanstack/react-virtual.

Testing UI

Visual Regression Testing

  • Use Chromatic or Percy to capture screenshots on every PR.
  • Storybook stories double as visual test cases -- one story per state (default, loading, error, empty, mobile, dark mode).
  • Review visual diffs before merging.

Component Testing

// Vitest + Testing Library: test behavior, not implementation
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { Button } from "./Button";

test("shows loading spinner and disables button when loading", async () => {
  render(<Button loading>Submit</Button>);
  expect(screen.getByRole("button")).toBeDisabled();
  expect(screen.getByRole("button")).toHaveAttribute("aria-busy", "true");
});

test("calls onClick when clicked", async () => {
  const onClick = vi.fn();
  render(<Button variant="primary" size="md" onClick={onClick}>Save</Button>);
  await userEvent.click(screen.getByRole("button", { name: "Save" }));
  expect(onClick).toHaveBeenCalledOnce();
});

Accessibility Audits

Automated tools catch approximately 30-50% of accessibility issues. Use them as a baseline, not a finish line.

# Lighthouse CI in your pipeline
npx @lhci/cli autorun --collect.url=http://localhost:3000

# axe-core in tests
npm install --save-dev @axe-core/react   # runtime warnings in dev
npm install --save-dev jest-axe           # assertions in tests
// axe-core in component tests
import { axe, toHaveNoViolations } from "jest-axe";
expect.extend(toHaveNoViolations);

test("form has no accessibility violations", async () => {
  const { container } = render(<ApplyForm />);
  const results = await axe(container);
  expect(results).toHaveNoViolations();
});

Responsive Testing

  • Test at 320px (small phone), 375px (standard phone), 768px (tablet), 1024px (small desktop), and 1440px (large desktop).
  • Use Playwright's page.setViewportSize() to automate responsive checks.
  • Test touch interactions on real devices -- emulators miss gesture nuances.

Design Thinking Phase

Before writing code, commit to a bold aesthetic direction. Generic "AI slop" aesthetics — purple gradients on white, centered layouts, Inter/Roboto fonts — are the fastest way to make a project look like every other AI-generated site.

Pre-Code Checklist

1. PURPOSE: What problem does this interface solve? Who uses it?
2. TONE: Pick a direction and commit — brutally minimal, maximalist,
   retro-futuristic, organic/natural, luxury/refined, editorial,
   brutalist/raw, art deco, soft/pastel, industrial, playful.
3. CONSTRAINTS: Framework, performance budget, accessibility level.
4. DIFFERENTIATOR: What's the one thing someone will remember?

Bold maximalism and refined minimalism both work. The key is intentionality, not intensity.

Distinctive Typography

Choose fonts that have character. Pair a distinctive display font with a refined body font.

NEVER default to:
  Inter, Roboto, Arial, system-ui, sans-serif

INSTEAD choose fonts with personality:
  Display: Space Grotesk, Clash Display, Satoshi, Cabinet Grotesk,
           General Sans, Switzer, Zodiak, Gambetta
  Body: Outfit, Plus Jakarta Sans, DM Sans, Figtree, Geist

Pair a distinctive display font with a refined body font.
Vary choices between projects — never converge on the same pairing.

Color & Theme Commitment

/* Use CSS variables for consistency. Dominant color with sharp accent. */
:root {
  --color-dominant: #1a1a2e;   /* 60-70% visual weight */
  --color-support: #16213e;    /* supporting tone */
  --color-accent: #e94560;     /* sharp accent — used sparingly */
}

Dominant colors with sharp accents outperform timid, evenly-distributed palettes. Commit to a cohesive palette — do not spread colors equally.

Motion & Micro-Interactions

High-impact moments over scattered effects:
  → One well-orchestrated page load with staggered reveals
    (animation-delay) creates more delight than random micro-interactions
  → Scroll-triggered reveals and hover states that surprise
  → Prefer CSS-only animations for HTML; use Motion (Framer Motion)
    for React when available

Spatial Composition

Break out of predictable layouts: asymmetry, overlap, diagonal flow, grid-breaking elements, generous negative space OR controlled density. Unexpected layouts make interfaces memorable.

Backgrounds & Visual Details

Create atmosphere and depth instead of defaulting to flat solid colors:

Consider: gradient meshes, noise textures, geometric patterns,
layered transparencies, dramatic shadows, decorative borders,
custom cursors, grain overlays.
Match the effect to the overall aesthetic — not every project
needs every technique.

Anti-Patterns

Anti-PatternWhy It HurtsDo This Instead
Div soup (<div> for everything)No semantic meaning, broken screen reader experience, poor SEOUse <header>, <nav>, <main>, <section>, <article>, <button>, <a>
Ignoring touch targets (32px buttons)Mis-taps frustrate users, WCAG violationMinimum 44x44px with 8px spacing between targets
Flash of Unstyled Text (FOUT/FOIT)Layout shift, invisible text during loadfont-display: swap + preload critical fonts + size-adjust fallback
Layout shift from images without dimensionsCLS penalty, visual jankAlways set width/height or aspect-ratio on images and media
Inaccessible modals (no focus trap)Keyboard users get stuck, screen readers read background contentUse native <dialog>, trap focus, close on Escape, restore focus on close
CSS !important everywhereSpecificity wars, unmaintainable stylesheetsFix the cascade: use layers, lower specificity selectors, or Tailwind utilities
Client-fetching data that could be server-renderedSlower LCP, loading spinners, duplicated logicUse React Server Components or getServerSideProps for initial data
Inline styles for responsive designCannot use media queries or container queries, duplicated valuesUse utility classes (Tailwind) or CSS custom properties
Disabling zoom (user-scalable=no)WCAG violation, excludes users with low visionRemove it. Let users zoom. Design to accommodate zoomed-in layouts
Building custom components that already exist nativelyLarger bundle, more bugs, worse accessibilityUse native <select>, <details>, <dialog>, <input type="date"> first

Power Move

"Audit this page for frontend quality. Check these dimensions and give me a scorecard with pass/fail and specific fixes:
1. Mobile responsiveness: test at 320px, 375px, 768px, 1024px
2. Touch targets: are all interactive elements at least 44x44px?
3. Accessibility: run axe-core, check heading hierarchy, verify keyboard navigation, test with VoiceOver
4. Performance: measure LCP, INP, CLS via Lighthouse -- flag anything outside 'Good' thresholds
5. Semantic HTML: identify any div-soup or missing landmarks
6. Loading states: does every async view handle loading, empty, and error?
7. Dark mode: does the color scheme respect prefers-color-scheme and maintain contrast ratios?
8. Image optimization: are images using modern formats, explicit dimensions, and lazy loading?
Prioritize fixes by user impact. Ship the top 3 fixes first."

The agent becomes your frontend quality gate -- accessibility, performance, and mobile experience validated before the PR merges.

Install

Download ZIP
Requires askill CLI v1.0+

AI Quality Score

88/100Analyzed 2/23/2026

Comprehensive frontend UI/UX skill with detailed coverage of mobile-first design, component architecture, CSS patterns, accessibility, and performance optimization. Well-structured with practical code examples and actionable guidance. Slight mismatch between tags and content, but the technical content itself is high-quality and reusable.

95
90
85
85
90

Metadata

Licenseunknown
Version-
Updated2/18/2026
Publisherpingwu

Tags

apici-cdobservabilitypromptingtesting