Micro-interactions skill for Codex
Feedback that answers did-it-work in 100 ms. Codex defines each micro-interaction by trigger, rule, feedback and reset, fills in a full state matrix for every control, and implements feedback that appears within 100 ms using transform and opacity only. It gates hover to hover-capable pointers, rolls back failed optimistic updates, announces async results to screen readers and keeps feedback visible under reduced motion.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- micro-interactions Installs to ~/.agents/skills/micro-interactions/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install micro-interactions The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project micro-interactions Installs to .agents/skills/micro-interactions/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/micro-interactions.zipunzip -o micro-interactions.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) micro-interactions PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
104-line POSIX script. No sudo, no telemetry. Read install.sh
Use it in Codex
Start a prompt with $micro-interactions, or pick it from /skills.
You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.
What Codex reads before loading it
Designs and implements purposeful micro-interactions - full state matrices for controls (hover, focus-visible, pressed, loading, success, error), feedback within 100 ms, optimistic updates with rollback, toggles, copy-to-clipboard, inline confirmations, entry and exit transitions with @starting-style, hover gated to hover-capable pointers, and prefers-reduced-motion fallbacks. Use when buttons or controls feel dead or laggy, adding hover and press states, building toggles, likes, copy buttons or inline saves, or when asked to make an interface feel more responsive or polished.
Use it when
- Controls lack hover, pressed, focus or loading feedback
- Actions feel slow even when the server is fast
- Building toggles, copy buttons, inline saves or undo patterns
- Popovers and menus appear or vanish abruptly
Use something else when
- Easing, duration and choreography for larger UI motion: use motion-design-principles
- Keyframes and scroll-driven animation implementation details: use css-animations
How to check the result
Every control shows distinct hover, focus-visible, pressed and loading states, feedback appears within 100 ms, and with reduced motion emulated the feedback is still visible without movement.
The mistake it prevents
Leaving hover styles ungated, so on touch screens the last tapped button stays highlighted as if still hovered.
A micro-interaction earns its place when it answers a question the user has at that moment: “Did it register?”, “Is it working?”, “Did it succeed?”, “What changed?”. It must appear within 100 ms of the input, finish quickly enough never to block the next action, and degrade to an instant state change under reduced motion. Animation that answers nothing is decoration and costs attention.
Anatomy
For each interaction, define four parts before writing CSS:
| Part | Question | Example (copy button) |
|---|---|---|
| Trigger | What starts it? | Click or Enter/Space on the button |
| Rule | What happens? | Text goes to clipboard |
| Feedback | How does the user know? | Icon swaps to a check, label “Copied”, announced to screen readers |
| Loop / reset | What happens after? | Reverts after 2 s; repeated clicks restart the timer |
State matrix
Every interactive control needs these states designed, not left to browser defaults:
| State | Selector / attribute | Typical treatment |
|---|---|---|
| Rest | default | Base token colors |
| Hover | @media (hover: hover) { :hover } | Background shift of about 4–8% lightness |
| Focus | :focus-visible | 2 px outline, 2 px offset, 3:1 contrast against surroundings |
| Pressed | :active, aria-pressed="true" for toggles | Darker shift plus scale(.97) or 1 px translate |
| Disabled | :disabled, aria-disabled="true" | Reduced contrast, cursor: not-allowed, still readable |
| Loading | aria-busy="true" | Spinner or progress replaces icon; label keeps width |
| Success / error | data attribute or class | Brief icon and color change, then reset or persist |
| Selected / current | aria-selected, aria-current | Persistent, not just hover-like |
.btn {
transition: background-color 120ms ease-out, transform 120ms ease-out, box-shadow 120ms ease-out;
}
@media (hover: hover) {
.btn:hover { background: oklch(from var(--btn-bg) calc(l - 0.05) c h); }
}
.btn:active { transform: scale(.97); transition-duration: 60ms; }
.btn:focus-visible { outline: 2px solid var(--color-focus); outline-offset: 2px; }
.btn[aria-busy="true"] { pointer-events: none; }
@media (prefers-reduced-motion: reduce) {
.btn, .btn:active { transition: none; transform: none; }
}
(hover: hover) stops sticky hover states on touch screens, where the last tapped button
keeps its hover color.
Timing
| Interaction | Duration | Easing |
|---|---|---|
| Hover, press, focus color | 80–150 ms | ease-out |
| Toggle, checkbox, small icon swap | 150–200 ms | ease-out or a gentle spring |
| Dropdown, popover, tooltip enter | 150–220 ms | cubic-bezier(.2,.8,.2,1) |
| Exit of the same | 100–150 ms (faster than enter) | ease-in |
| Toast, inline banner | 200–300 ms | decelerate |
| Success hold before reset | 1.5–2.5 s | none |
Respond visually within 100 ms; keep Interaction to Next Paint at 200 ms or less. If the
real work takes longer, show progress immediately and finish later. For choreography of
larger transitions, see motion-design-principles.
Patterns
Optimistic toggle with rollback
function LikeButton({ id, initial }: { id: string; initial: boolean }) {
const [liked, setLiked] = useState(initial);
const [error, setError] = useState<string | null>(null);
async function toggle() {
const next = !liked;
setLiked(next); // instant feedback
setError(null);
try {
await api.setLike(id, next);
} catch {
setLiked(!next); // roll back
setError("Couldn't save. Try again.");
}
}
return (
<>
<button type="button" aria-pressed={liked} onClick={toggle} className="like">
<HeartIcon aria-hidden="true" /> {liked ? "Liked" : "Like"}
</button>
<span role="status" className="sr-only">{error ?? ""}</span>
</>
);
}
Use optimistic updates only when the action almost always succeeds and is reversible.
Payments, deletes and sends wait for confirmation. React 19’s useOptimistic covers the
same pattern inside actions.
Copy to clipboard
button.addEventListener("click", async () => {
try {
await navigator.clipboard.writeText(code.textContent);
button.dataset.state = "copied";
status.textContent = "Copied to clipboard";
} catch {
button.dataset.state = "error";
status.textContent = "Copy failed. Select the text and copy manually.";
}
clearTimeout(button._t);
button._t = setTimeout(() => { button.dataset.state = ""; status.textContent = ""; }, 2000);
});
status is an element with role="status" that exists before the click, so screen
readers announce the change. The Clipboard API requires a secure context (HTTPS or
localhost).
Entry and exit without JavaScript timers
.popover {
transition: opacity 180ms ease-out, translate 180ms ease-out, display 180ms allow-discrete, overlay 180ms allow-discrete;
opacity: 1; translate: 0 0;
}
.popover:not(:popover-open) { opacity: 0; translate: 0 -4px; }
@starting-style {
.popover:popover-open { opacity: 0; translate: 0 -4px; }
}
@starting-style defines the “before” state for elements entering from display:none,
and allow-discrete lets display and overlay flip at the end of the exit. Both are
supported in current evergreen browsers; without them, the popover simply appears
instantly, which is an acceptable fallback.
Inline save feedback
For autosaving fields, show a small status next to the field: “Saving…” during the request, “Saved” with a check for 2 s, or a persistent error with retry. Never a toast for every keystroke save.
Destructive actions
Prefer undo over confirmation for reversible deletes: remove immediately, show “Deleted. Undo” for 5–8 s, commit after. Use a confirmation dialog only for irreversible actions, and name the object in the button (“Delete 3 invoices”).
Rules
- Animate
transformandopacity; avoid animatingwidth,height,top,left, which trigger layout on every frame. - Keep labels from shifting width when states change (reserve space or swap within a fixed-width box).
- One feedback channel per event is usually enough; color + icon + motion + toast for one click is noise.
- Under
prefers-reduced-motion: reduce, replace movement with instant state or opacity fades; do not remove the feedback itself. - Feedback must not rely on color alone; pair with icon or text.
- Focus never moves unexpectedly as a side effect of a micro-interaction.
Workflow
- [ ] 1. List controls and the user question each interaction answers
- [ ] 2. Fill the state matrix per control (design tokens for every state)
- [ ] 3. Implement with transitions on transform/opacity/color only
- [ ] 4. Add reduced-motion and hover-capability guards
- [ ] 5. Add screen-reader announcements for async results
- [ ] 6. Verify timing, INP and accessibility
Verification
- Slow animations in Chrome DevTools (Animations panel, 10% speed) to inspect easing and overlaps.
- Emulate reduced motion (DevTools Rendering panel, or Playwright
page.emulateMedia({ reducedMotion: "reduce" })): feedback still visible, no movement. - Keyboard-only pass: every state reachable and visible, including focus on pressed and loading buttons.
- Throttle network to Slow 4G: loading states appear within 100 ms; rollback works when the request fails (block the URL in DevTools).
- Check INP in the Performance panel for the busiest interaction; target 200 ms or less.
Output contract
**Interactions:** <control>: trigger / rule / feedback / reset
**States implemented:** hover, focus-visible, pressed, disabled, loading, success, error
**Timing tokens:** fast 120 ms, base 180 ms, slow 280 ms; easing <values>
**Reduced motion:** <behavior> **Announcements:** <role=status regions>
**Checks:** keyboard ok, reduced motion ok, failure rollback ok, INP <n> ms What the zip contains
SKILL.md plus the interface file below. Nothing runs on install.
agents/openai.yaml
Gives the Codex app a display name, a one-line description and a starter prompt for this skill.
interface:
display_name: "Micro-interactions"
short_description: "Feedback that answers did-it-work in 100 ms"
default_prompt: "$micro-interactions Our buttons feel dead, add proper hover, press, focus and loading states" About this skill
Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.