Interface polish
Skill Gekkos-tech/agency-os/plugins/design-foundation/skills/interface-polish
The complete Claude Code toolkit for web & advertising design agencies — 6 plugin bundles for the full agency workflow
npx -y skills add Gekkos-tech/agency-os --skill interface-polishAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
2 things to look at
- 26 days oldThe repository was created 26 days ago. New is not bad, but a brand new repository carrying a familiar-sounding name is the shape a typosquat arrives in, and there has been no time for anyone else to find a problem with it.
- 4 stars4 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Design-engineering micro-detail pass for interfaces that already work but feel unrefined. Applies concrete CSS/JS rules for hover/active/focus states, transition timing and easing, enter/exit animations, stagger patterns, layered shadows, border-vs-shadow elevation, nested border-radius, optical alignment, typography details (tabular numbers, letter-spacing, text-wrap balance), image treatment, scroll behavior, and reduced-motion support. Use when the user says "polish", "make it feel better", "it feels off", "micro-interactions", "finishing touches", "add the last 10 percent", or when a UI is functionally done but flat. NOT for full visual overhauls of unrefined designs (use redesign), NOT for choosing a visual direction or creative concept (use taste or style-systems) - this skill refines execution details within an already-chosen design.
SKILL.md
13.6 KB, as published. Nobody here has run it
Interface Polish
The last 10% that separates "works" from "feels great". Apply these rules to an interface whose layout, palette, and structure are already decided. Do not change the design direction — refine its execution.
Work through the sections as a checklist against the actual code. Fix what's missing; leave what's already right.
1. Interactive states
Every interactive element needs four distinct states: rest, hover, active, focus-visible. Missing states are the #1 "feels unfinished" signal.
.button {
transition: background-color 150ms ease, box-shadow 150ms ease, transform 100ms ease;
}
.button:hover { background-color: var(--accent-hover); }
.button:active { transform: scale(0.98); transition-duration: 50ms; } /* press responds FASTER than hover */
.button:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px; /* never outline: none without a replacement */
}
Rules:
- Hover changes must be subtle but unmistakable: one step of background shift, or 1–2px lift with shadow deepening — not both plus a color change.
:activeshould feel physically pressed: scale 0.97–0.98 or translateY(1px), with a shorter duration than hover.- Use
:focus-visible, not:focus, so mouse clicks don't paint rings but keyboards do. - Rows/list items: hover =
background-colorshift only; don't move them. - Disabled: reduce opacity to ~0.5 AND remove hover effects (
pointer-events: noneor explicit rules). - Touch: hover states must not be load-bearing; anything revealed on hover needs a touch/focus path too.
2. Transition timing and easing
- Durations: 100–150ms hover/press · 200ms dropdowns, tooltips · 250–300ms modals, drawers, layout shifts · 400–600ms only for page-level or decorative reveals. When unsure, subtract 50ms — slow transitions read as lag.
- Never
transition: all. List properties explicitly;allcauses accidental animation of layout properties and jank. - Easing:
ease-outfor things appearing/responding to the user (start fast, settle).ease-inorease-in-outfor things leaving.cubic-bezier(0.16, 1, 0.3, 1)(expo-out) makes entrances feel expensive. Bounce/overshoot only if the design system is playful — and subtle (≤8% past target). - Exit faster than enter: a modal opening at 250ms should close in ~180ms. Users wait for what appears; they've already dismissed what's leaving.
- Animate only compositor-friendly properties where possible:
transform,opacity. Animatingwidth/height/top/left/margincauses layout jank; if unavoidable, keep the element small or usetransformtricks. Never animatebox-shadowdirectly on large surfaces — crossfade a pseudo-element's opacity instead:
.card::after {
content: ""; position: absolute; inset: 0; border-radius: inherit;
box-shadow: var(--shadow-lg); opacity: 0; transition: opacity 200ms ease;
pointer-events: none;
}
.card:hover::after { opacity: 1; }
3. Enter/exit animations
Elements should arrive from somewhere, not pop into existence.
- Pattern: fade + small movement. Opacity 0→1 plus translateY(8–12px)→0, or scale(0.96–0.98)→1 for popovers/modals. Movement over ~16px looks theatrical.
- Origin matters: dropdowns scale/slide from the trigger (
transform-origin: top leftor as appropriate), toasts slide from the edge they live on, modals scale from center. - Exit animations require keeping the node until the animation ends. In CSS-only setups, prefer
@starting-style+transition-behavior: allow-discrete(modern) or toggle a class and remove ontransitionend. In React, use a presence utility or a two-step state (closing → closed).
dialog {
opacity: 1; transform: scale(1);
transition: opacity 200ms ease-out, transform 200ms cubic-bezier(0.16, 1, 0.3, 1),
overlay 200ms allow-discrete, display 200ms allow-discrete;
}
@starting-style { dialog[open] { opacity: 0; transform: scale(0.97); } }
dialog:not([open]) { opacity: 0; transform: scale(0.98); }
- Height-into-view (accordions): animate
grid-template-rows: 0fr → 1fron a grid wrapper withmin-height: 0on the child — smooth without measuring heights.
4. Stagger
Lists that animate in as one block feel cheap; a small cascade feels orchestrated.
- Delay step: 30–60ms between items. Total cascade should finish under ~600ms — cap the delay after 8–10 items (
min(index, 8) * 40ms). - Stagger only on first entrance (page load, list appearing) — not on every re-render or filter change.
.list > * { animation: item-in 350ms cubic-bezier(0.16, 1, 0.3, 1) backwards; }
.list > :nth-child(1) { animation-delay: 0ms; }
.list > :nth-child(2) { animation-delay: 40ms; }
/* or dynamically: */
document.querySelectorAll(".list > *").forEach((el, i) => {
el.style.animationDelay = `${Math.min(i, 8) * 40}ms`;
});
@keyframes item-in { from { opacity: 0; transform: translateY(10px); } }
animation-fill-mode: backwards prevents the pre-delay flash of the final state.
5. Shadows and elevation
- Single-layer shadows look flat. Layer a tight contact shadow with a soft ambient one:
--shadow-md: 0 1px 2px rgba(16, 24, 40, 0.06), /* contact: small blur, small offset */
0 8px 24px rgba(16, 24, 40, 0.10); /* ambient: large blur, large offset */
- Shadow color: tint toward the background/brand hue instead of pure black —
rgba(16,24,40,…)on cool UIs, warm equivalents on warm UIs. Pure black shadows look dirty on colored backgrounds. - Y-offset grows with elevation; blur ≈ 2–3× offset; opacity DROPS as blur grows.
- Border vs shadow: static containers on a page → 1px border (or background shift), no shadow or a whisper (
--shadow-xs). Floating ephemerals (menus, popovers, modals, drag previews) → shadow, plus a hairline border so edges survive on white-on-white. Shadow communicates "above the page"; if it doesn't float, it shouldn't cast much. - Dark mode: shadows nearly disappear against dark backgrounds — convey elevation with lighter surface colors and borders instead; keep only a faint shadow for grounding.
- Inset highlight for a crisp premium edge on raised elements:
box-shadow: inset 0 1px 0 rgba(255,255,255,0.08)(dark UIs) as an extra layer.
6. Border-radius nesting
Concentric corners: inner radius = outer radius − padding. A 16px-radius card with 8px padding needs 8px-radius children; giving them 16px too makes corners visibly clash.
.card { border-radius: 16px; padding: 8px; }
.card > .thumb { border-radius: calc(16px - 8px); } /* = 8px */
- If the math gives ≤2px, use a small consistent value (2–4px) rather than 0 next to a large outer radius.
- Never mix radius scales on siblings (a 4px button beside a 12px button).
- Pills inside rounded containers are exempt (radius 999px reads as its own shape).
- Elements clipped at a rounded parent's edge need
overflow: hiddenon the parent or matching radius on the child, or corners will poke out.
7. Optical alignment
Mathematical centering is often visually wrong; trust the eye and nudge.
- Play/triangle icons in circles: shift right ~1–2px (visual mass sits left of the bounding box).
- Icons beside text: align to cap-height/x-height feel, not baseline math — usually
translateY(-0.5px – 1px)or flexboxalign-items: centerplus a nudge. - Text in buttons: many fonts render slightly low;
padding-bottom: 1pxless than top, or line-height tuning, makes labels look centered. - Large display text: the bounding box includes side bearings; pull the first line left with
margin-left: -0.05emto flush-align it with body text below. - Chevrons/arrows following labels: optical gap ≈ 4–6px, less than the grid would suggest.
- Rule of thumb: when something "looks off-center" but the inspector says it's centered, the inspector is wrong. Nudge by 1–2px and re-check by eye (or screenshot).
8. Typography details
- Tabular numbers wherever digits change or align (tables, timers, counters, prices in lists):
font-variant-numeric: tabular-nums;— otherwise columns wobble and timers jitter. - Letter-spacing scales inversely with size: large display text needs negative tracking (
-0.01emto-0.03emabove ~32px); small ALL-CAPS labels need positive tracking (0.04em–0.08em). Body text: none. text-wrap: balanceon headings (≤4 lines);text-wrap: prettyon paragraphs to prevent orphan words.- Font smoothing: on macOS, light-on-dark text renders heavy;
-webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;slims it. Apply globally on dark UIs; use judgment on light ones. - Line-height pairs inversely with size: body 1.5–1.65, headings 1.1–1.2, giant display 0.95–1.05.
- Truncation: single line
text-overflow: ellipsis; white-space: nowrap; overflow: hidden;; multi-line-webkit-line-clamp. Always addtitleor a tooltip for truncated content. - Hyphens/
overflow-wrap: anywhereon user-generated strings so long tokens can't blow layouts. - Interpolate
font-variation-settings/weight rather than swapping weights on hover (weight-swap causes width jump; if you must, reserve width with a bold hidden twin).
9. Image treatment
- Subtle inset outline stops light images from bleeding into light backgrounds:
.img-frame { position: relative; }
.img-frame::after {
content: ""; position: absolute; inset: 0; border-radius: inherit;
box-shadow: inset 0 0 0 1px rgba(16, 24, 40, 0.08);
pointer-events: none;
}
- Reserve space to kill layout shift: always set
width/heightattributes oraspect-ratioon the container;object-fit: coverfor fills. - Background-color placeholder (
background: var(--surface-2)) behind every image so slow loads show a designed surface, not a void. - Avatars: add the inset ring above plus a background color for missing images; never let a broken-image glyph render.
- Hover zoom belongs inside an
overflow: hiddenframe: image scales 1→1.03–1.05, frame stays put. loading="lazy"below the fold; never on the LCP/hero image.
10. Scroll behavior
scroll-margin-topon anchor targets so sticky headers don't cover them:[id] { scroll-margin-top: calc(var(--header-h) + 16px); }scroll-behavior: smoothfor in-page anchors — gate it behind reduced-motion (see §11).- Horizontal scrollers (chip rows, card rails):
scroll-snap-type: x mandatory+scroll-snap-align: start, hide scrollbar only if arrows/affordances exist, addoverscroll-behavior-x: contain. - Modals/drawers: lock body scroll while open (
overflow: hiddenon body oroverscroll-behavior: containon the panel) — background scroll bleed feels broken. - Custom scrollbars: thin and unobtrusive is fine (
scrollbar-width: thin; scrollbar-color: var(--border) transparent); never remove scrollbars from long content. - Scroll-triggered reveals: threshold ~0.15–0.25, animate once, never re-hide on scroll-up:
const io = new IntersectionObserver((entries) => {
for (const e of entries) if (e.isIntersecting) { e.target.classList.add("in"); io.unobserve(e.target); }
}, { threshold: 0.2 });
document.querySelectorAll("[data-reveal]").forEach((el) => io.observe(el));
11. Reduced motion
Non-negotiable. Every animation added by this pass must respect prefers-reduced-motion.
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}
- The blanket rule above is the safety net; prefer targeted alternatives where motion carries meaning (e.g. keep the fade, drop the movement).
- In JS animation code, check
matchMedia("(prefers-reduced-motion: reduce)").matchesbefore starting loops, parallax, or auto-playing sequences. - Opacity fades are generally safe under reduced motion; large translations, scaling, parallax, and auto-scroll are not.
Final pass checklist
- All interactive elements: hover, active, focus-visible, disabled states present and distinct.
- No
transition: all; all durations ≤300ms in-app; exits faster than entrances. - Entering elements fade+move ≤16px; ephemerals originate from their trigger.
- First-load lists stagger ≤60ms/item, capped.
- Shadows layered and tinted; borders on static, shadows on floating; dark mode uses surface steps.
- Nested radii follow inner = outer − padding.
- Icon/text alignment checked by eye, not just by flexbox.
- Tabular nums on data; tracking tightened on display, widened on caps labels; headings balanced.
- Images: aspect-ratio reserved, placeholder background, inset outline where needed.
- Anchor scroll-margin set; body locked under modals; reveals fire once.
-
prefers-reduced-motionhandled globally and in JS.