plugins/frontend-design/skills/frontend-design/SKILL.md
Use when user asks to build a web component, page, or application, or when the task involves frontend design, HTML/CSS generation, or UI layout. Applies specific rules for typography, OKLCH color, layout, motion, interaction, and UX writing to produce distinctive, production-grade interfaces that avoid generic AI aesthetics.
npx skillsauth add harnessprotocol/harness-kit frontend-designInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
This skill guides creation of distinctive, production-grade frontend interfaces that avoid generic "AI slop" aesthetics. Implement real working code with exceptional attention to aesthetic details and creative choices.
Design without project context produces generic output. Before designing anything:
Before shipping: "If someone saw this and said 'AI made this,' would they believe it immediately?"
If yes — fix it. The fingerprints of AI-generated work from 2024–2025:
ease as the default easing functionA distinctive interface makes someone ask "how was this made?" not "which AI made this?"
Commit to a bold, specific aesthetic direction before touching code:
Bold maximalism and refined minimalism both work. The key is intentionality, not intensity.
Font selection — avoid the invisible defaults:
| Instead of | Use |
|-----------|-----|
| Inter | Instrument Sans, Plus Jakarta Sans, Outfit |
| Roboto | Onest, Figtree, Urbanist |
| Open Sans | Source Sans 3, DM Sans |
| Anything generic | Fraunces / Newsreader / Lora (editorial/premium) |
| Any above | -apple-system, BlinkMacSystemFont, system-ui (native feel, zero cost) |
You often don't need a second font. One family in multiple weights creates cleaner hierarchy than two competing typefaces. When pairing, contrast on multiple axes: serif + sans, geometric + humanist, condensed display + wide body. Never pair two fonts that are similar but not identical.
Modular scale: Use fewer sizes with more contrast. A 5-step system covers everything: xs (0.75rem) / sm (0.875rem) / base (1rem) / lg (1.25–1.5rem) / xl+ (2–4rem). Common ratios: 1.25, 1.333, 1.5. Pick one and commit.
Fluid type: Use clamp(min, preferred, max). Don't use on button text, labels, or UI elements.
Line-height is the base unit for ALL vertical spacing. If body text is 16px at 1.5 line-height = 24px, spacing values should be multiples of 24px. Creates subconscious mathematical harmony.
Readable measure: max-width: 65ch on paragraphs. Light text on dark: add 0.05–0.1 to line-height.
OpenType features (most devs forget these):
.data-table { font-variant-numeric: tabular-nums; } /* aligned numbers */
abbr { font-variant-caps: all-small-caps; }
code { font-variant-ligatures: none; }
Prevent font layout shift — define fallback font metrics:
@font-face {
font-family: 'CustomFont-Fallback';
src: local('Arial');
size-adjust: 105%;
ascent-override: 90%;
descent-override: 20%;
}
body { font-family: 'CustomFont', 'CustomFont-Fallback', sans-serif; }
Rules: Never px for body text (use rem/em). Never disable zoom. Minimum 16px body. 44px+ touch targets for text links.
Use OKLCH, not HSL. HSL is not perceptually uniform — 50% lightness in yellow looks bright, in blue looks dark. OKLCH equal steps look equal.
--color-primary: oklch(60% 0.15 250);
--color-primary-light: oklch(85% 0.08 250); /* reduce chroma at extremes */
--color-primary-dark: oklch(35% 0.12 250);
Always tint neutrals toward brand hue — pure gray doesn't exist in nature:
--gray-100: oklch(95% 0.01 250); /* chroma 0.01 — tiny but creates cohesion */
--gray-900: oklch(15% 0.01 250);
Palette structure: Primary (1 color, 3–5 shades) + Neutral (9–11 shades) + Semantic (success/error/warning/info) + Surface (2–3 elevation levels). Skip secondary/tertiary unless genuinely needed.
60-30-10 rule: 60% neutral backgrounds / 30% secondary (text, borders, inactive states) / 10% accent (CTAs, focus states). Accent colors work because they're rare. Overuse kills the power.
Contrast minimums: Body text 4.5:1 (AA). Large text (18px+) 3:1. UI components 3:1. Placeholder text still needs 4.5:1 — that common light gray placeholder almost always fails.
Gray on color always fails — never use gray text on a colored background. Use a darker shade of the background color instead.
Dark mode is NOT inverted light mode:
:root[data-theme="dark"] {
--surface-1: oklch(15% 0.01 250);
--surface-2: oklch(20% 0.01 250); /* lighter surface = higher elevation */
--surface-3: oklch(25% 0.01 250);
--body-weight: 350; /* perceived weight is lighter on dark — reduce slightly */
}
No pure black backgrounds (use oklch 12–18%). No shadows for elevation — use lighter surface colors. Desaturate accents slightly. Never #000 or #fff anywhere.
Token hierarchy: Primitive (--blue-500) + semantic (--color-primary: var(--blue-500)). Dark mode only redefines the semantic layer.
Heavy use of alpha (rgba) is a design smell — usually means an incomplete palette. Define explicit colors per context instead.
4pt base spacing (4, 8, 12, 16, 24, 32, 48, 64, 96px) — 8pt is too coarse. Name tokens by relationship: --space-sm, --space-lg (not --spacing-8). Use gap instead of margins for siblings.
The Squint Test: Blur your eyes. Can you identify the most important element? Second most? Clear groupings? If everything looks the same weight blurred — hierarchy problem.
Hierarchy uses multiple dimensions simultaneously:
| Dimension | Strong | Weak | |-----------|--------|------| | Size | 3:1+ ratio | <2:1 | | Weight | Bold vs Regular | Medium vs Regular | | Color | High contrast | Similar tones | | Position | Top/left | Bottom/right | | Space | Surrounded by whitespace | Crowded |
A heading that's larger AND bolder AND has more space above it is strong hierarchy. One dimension alone is weak.
Cards are not required. Spacing + alignment create visual grouping naturally. Use cards only when content needs explicit comparison, discrete actionability, or clear interaction boundaries. Never nest cards inside cards — use spacing, typography, and subtle dividers for within-card hierarchy.
Asymmetry feels designed — left-aligned text with asymmetric layouts beats everything centered. Create visual rhythm through varied spacing (tight groupings, generous separations), not the same padding everywhere. Break the grid intentionally for emphasis.
Container queries for components, not viewport queries:
.card-container { container-type: inline-size; }
@container (min-width: 400px) {
.card { grid-template-columns: 120px 1fr; }
}
A card in a narrow sidebar stays compact; in main content it expands — automatically, without viewport hacks.
Self-adjusting grid (no breakpoints needed):
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); }
Touch targets — visual size ≠ tap target (44px minimum):
.icon-button { width: 24px; height: 24px; position: relative; }
.icon-button::before { content: ''; position: absolute; inset: -10px; }
Optical alignment: Text at margin-left: 0 looks slightly indented — use margin-left: -0.05em to optically align.
Duration rule (100 / 300 / 500):
| Duration | Use | |----------|-----| | 100–150ms | Button press, toggle, color change | | 200–300ms | Menu open, tooltip, hover state | | 300–500ms | Accordion, modal, drawer | | 500–800ms | Page load, hero reveals |
Exit animations at ~75% of enter duration.
Easing — never use the ease keyword (it's a compromise):
--ease-out: cubic-bezier(0.16, 1, 0.3, 1); /* elements entering */
--ease-in: cubic-bezier(0.7, 0, 0.84, 0); /* elements leaving */
--ease-in-out: cubic-bezier(0.65, 0, 0.35, 1); /* state toggles */
/* Default for micro-interactions — natural deceleration */
--ease-out-quart: cubic-bezier(0.25, 1, 0.5, 1);
--ease-out-expo: cubic-bezier(0.16, 1, 0.3, 1);
Only animate transform and opacity — everything else causes layout recalculation.
For height animations: grid-template-rows: 0fr → 1fr instead of animating height.
Perceived performance: Our brains buffer ~80ms of sensory input — anything under feels instant. Optimistic UI (update immediately, sync later) applies for low-stakes actions (likes, follows). Never for payments or destructive operations.
Reduced motion is not optional — 35% of adults 40+ have vestibular disorders:
@media (prefers-reduced-motion: reduce) {
.card { animation: fade-in 200ms ease-out; } /* crossfade, not motion */
}
Every interactive element needs all 8 states: Default, Hover, Focus, Active, Disabled, Loading, Error, Success. Common miss: designing hover without focus. Keyboard users never see hover states.
Focus rings — never outline: none without replacement:
button:focus { outline: none; }
button:focus-visible {
outline: 2px solid var(--color-accent);
outline-offset: 2px;
}
Loading: Skeleton screens beat spinners — they preview content shape and feel faster. Optimistic updates for low-stakes actions. Specific copy: "Saving your draft..." not "Loading..."
Modals: Use native <dialog> (automatic focus trap + Escape). Use them sparingly — modals are almost always the lazy choice. The popover API handles tooltips/dropdowns/non-modal overlays without z-index wars.
Destructive actions: Undo > confirmation dialogs. Users click through confirmations mindlessly. Remove → show undo toast → delete after toast expires.
Forms: Placeholders are not labels. Validate on blur, not every keystroke. Errors below fields with aria-describedby.
Mobile-first: Start with mobile base styles, layer with min-width queries. Desktop-first (max-width) makes mobile load unnecessary styles.
Detect input method, not just screen size — a laptop can have a touchscreen; a tablet can have a keyboard:
@media (pointer: coarse) { .button { padding: 12px 20px; } } /* touch */
@media (hover: none) { .card { /* no hover states */ } } /* touch devices */
Handle the notch:
body { padding-bottom: max(1rem, env(safe-area-inset-bottom)); }
Enable: <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
Test on real devices — DevTools emulation misses actual touch interactions, CPU/memory constraints, font rendering differences, and browser chrome. At minimum: one real iPhone, one real Android.
Button labels — never "OK", "Submit", "Yes/No" — use specific verb + object:
| Bad | Good | |-----|------| | OK | Save changes | | Submit | Create account | | Yes | Delete message | | Cancel | Keep editing |
For destructive: name the destruction — "Delete 5 items" not "Delete selected".
Error message formula: What happened + Why + How to fix. Never blame the user.
"Email address isn't valid. Please include an @ symbol." not "Invalid input."
Empty states are onboarding moments: Acknowledge + explain value + provide action.
"No projects yet. Create your first one to get started." not "No items."
Consistency: One term per concept. Build a glossary. (Delete, not Delete/Remove/Trash.)
Never humor for errors — users are already frustrated. Be helpful, not cute.
Match complexity to vision. Maximalist designs need elaborate code with extensive animations. Minimalist designs need restraint, precision, and careful attention to spacing and subtle details. Elegance comes from executing the vision well, not from adding effects.
Make unexpected choices that feel genuinely designed for the context. No design should be the same. Vary between light and dark themes, different fonts, different aesthetics. NEVER converge on common choices across generations.
tools
Harness Kit documentation — installation, plugin catalog, creating plugins, cross-harness setup, architecture, and FAQ. Use when working with or configuring harness-kit plugins, understanding the plugin/skill system, installing slash commands, setting up AI coding tool configuration, answering questions about the plugin marketplace, writing SKILL.md files, using harness.yaml, or integrating with Copilot, Cursor, or Codex. Do NOT use for general Claude Code questions unrelated to harness-kit.
development
Use when user invokes /stats or asks about Claude Code usage, token consumption, session history, model distribution, or activity patterns. Generates an interactive HTML dashboard with charts and tables, auto-opens in browser. Also triggers on "how much have I used Claude", "show my usage", "token usage", "session stats", "usage report", "usage dashboard". Do NOT use for API billing or cost estimation — token counts are not costs.
development
Use when user invokes /review to review code changes. Also triggers on "review my code", "check this PR", "look at my changes". Accepts a PR number (e.g. /review 123), a path filter (e.g. /review src/auth/), or no argument (reviews current branch vs base). Produces structured per-file review with severity labels, cross-file analysis, and an overall verdict. Do NOT use for explaining what code does — use /explain instead.
testing
Use when user invokes /research command with any source — URL, GitHub repo, YouTube video, podcast, Reddit post, academic paper, documentation page, product site, local file, or empty. Processes and indexes research materials with raw source preservation and topic-level synthesis coalescing. Do NOT use for quick factual questions — use /explain instead.