Skip to content
Back to skills

Interface Feel

ASecurity

Make interfaces feel responsive and alive — micro-interactions, spring physics, press states, optimistic updates, skeleton loaders, cursor feedback, sound design hints, and delight moments. Use when asked to "make this feel better", "interface feels unresponsive", "add micro-interactions", "the UI feels dead", "springy animations", "delight", "optimistic UI", "skeleton screen", "cursor feedback", "feel more like a native app", "haptic feedback patterns", or "interface lacks personality". Do N...

  • 10 stars
  • 0 votes
  • 3 copies
  • 4 views
  • Added September 9, 2026
ai-agentsgoreactspringapiperformance

Works with

  • cursor
  • cli
  • api

Security analysis

A100/100

Scanned September 9, 2026

npx -y skills add yanacuti1121/Yana-AI --skill interface-feel --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Interface Feel?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Interface Feel
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/yanacuti1121-interface-feel/badge)](https://www.skillsdirectory.com/skills/yanacuti1121-interface-feel)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: interface-feel
description: >
  Make interfaces feel responsive and alive — micro-interactions, spring
  physics, press states, optimistic updates, skeleton loaders, cursor
  feedback, sound design hints, and delight moments. Use when asked to
  "make this feel better", "interface feels unresponsive", "add micro-interactions",
  "the UI feels dead", "springy animations", "delight", "optimistic UI",
  "skeleton screen", "cursor feedback", "feel more like a native app",
  "haptic feedback patterns", or "interface lacks personality". Do NOT use
  for: animation performance — see fixing-motion-performance. Do NOT use
  for: full animation system design — see motion-design.
origin: adapted:MIT © jakubkrehel
license: MIT © 2026 Vũ Văn Tâm
version: 1.0.0
compatibility: "React ≥ 18, Framer Motion v11. Patterns apply to any interactive UI."
---

## When to Use

- Use when: button clicks feel delayed or unacknowledged
- Use when: loading states show spinner but page feels frozen
- Use when: the interface feels like a form, not a product
- Use when: adding delight moments to a well-functioning but dry UI
- Do NOT use for: animation performance issues — see fixing-motion-performance
- Do NOT use for: typography and spacing polish — see impeccable

---

## Instant Feedback — The 100ms Rule

```
< 16ms  : imperceptible — system state change
< 100ms : instant — user perceives as immediate
< 300ms : fast — acceptable for animation
> 300ms : needs loader — user notices the wait
> 1000ms: needs progress — or user thinks it's broken
```

```tsx
// ✅ Respond in < 16ms — state changes before animation
<motion.button
  onPointerDown={() => setPressed(true)}  // visual response on pointer down, not click
  onPointerUp={() => setPressed(false)}
  animate={{ scale: pressed ? 0.96 : 1 }}
  transition={{ type: 'spring', stiffness: 800, damping: 25 }}
>
  Submit
</motion.button>
```

---

## Spring Physics

```tsx
// Spring vs duration-based: spring feels physical, duration feels programmed
// type: 'spring', stiffness, damping, mass — tune these three

// Snappy UI response (button, toggle)
const snappy = { type: 'spring', stiffness: 700, damping: 30 };

// Bouncy, playful (success state, notification badge)
const bouncy = { type: 'spring', stiffness: 400, damping: 10, mass: 0.8 };

// Smooth, heavy (modal, drawer)
const smooth = { type: 'spring', stiffness: 200, damping: 30, mass: 1.2 };

// Instant (toggle switch, checkbox)
const instant = { type: 'spring', stiffness: 2000, damping: 100 };
```

---

## Optimistic Updates

```tsx
// Don't wait for server — update UI immediately, roll back on error
function LikeButton({ postId, initialLiked }) {
  const [liked, setLiked] = useState(initialLiked);
  const [count, setCount] = useState(post.likes);

  async function handleLike() {
    const prev = { liked, count };
    setLiked(!liked);                     // immediate visual response
    setCount(c => liked ? c - 1 : c + 1);

    try {
      await api.toggleLike(postId);
    } catch {
      setLiked(prev.liked);               // rollback on failure
      setCount(prev.count);
      toast.error('Could not save — try again');
    }
  }

  return (
    <motion.button
      onClick={handleLike}
      animate={{ scale: liked ? [1, 1.3, 1] : 1 }}
      transition={{ duration: 0.25 }}
    >
      {liked ? '❤️' : '🤍'} {count}
    </motion.button>
  );
}
```

---

## Skeleton Screens (vs Spinners)

```tsx
// Spinners: "something is loading — don't know what"
// Skeletons: "here's where the content will land" — reduces layout shift

function ProductCardSkeleton() {
  return (
    <div className="animate-pulse space-y-3">
      <div className="h-48 bg-gray-200 rounded-lg" />        {/* image placeholder */}
      <div className="h-4 bg-gray-200 rounded w-3/4" />      {/* title */}
      <div className="h-3 bg-gray-200 rounded w-1/2" />      {/* subtitle */}
      <div className="h-8 bg-gray-200 rounded w-1/3" />      {/* price */}
    </div>
  );
}
```

```css
/* Custom shimmer — more polished than Tailwind animate-pulse */
@keyframes shimmer {
  0%   { background-position: -200% 0; }
  100% { background-position:  200% 0; }
}

.skeleton {
  background: linear-gradient(90deg,
    hsl(220 14% 92%) 25%,
    hsl(220 14% 96%) 50%,
    hsl(220 14% 92%) 75%
  );
  background-size: 200% 100%;
  animation: shimmer 1.5s infinite;
  border-radius: 4px;
}
```

---

## Cursor Feedback

```css
/* Cursor communicates affordance */
button, [role="button"], a, label[for] { cursor: pointer; }
input[type="text"], textarea           { cursor: text; }
[draggable="true"]                     { cursor: grab; }
[draggable="true"]:active              { cursor: grabbing; }
[disabled], [aria-disabled="true"]     { cursor: not-allowed; opacity: 0.5; }
```

---

## Delight Moments

```tsx
// Confetti on first-time success
import confetti from 'canvas-confetti';

function SuccessModal({ isFirstOrder }) {
  useEffect(() => {
    if (isFirstOrder) {
      confetti({ particleCount: 100, spread: 70, origin: { y: 0.6 } });
    }
  }, [isFirstOrder]);
  // ...
}

// Number count-up on stat reveal
<motion.span
  initial={{ opacity: 0 }}
  animate={{ opacity: 1 }}
  onAnimationComplete={() => controls.start({ value: targetValue })}
>
  <Counter from={0} to={targetValue} duration={1} />
</motion.span>
```

---

## Empty States That Invite Action

```tsx
// ❌ "No results found." — unhelpful, dead end
// ✅ Empty state with illustration + action + context
function EmptyTaskList({ onAdd }) {
  return (
    <motion.div
      initial={{ opacity: 0, y: 16 }}
      animate={{ opacity: 1, y: 0 }}
      className="flex flex-col items-center gap-4 py-16 text-center"
    >
      <TaskEmptyIllustration className="w-40 opacity-60" />
      <p className="text-muted-foreground text-sm">
        No tasks yet — add your first to get started.
      </p>
      <Button onClick={onAdd}>Add task</Button>
    </motion.div>
  );
}
```

---

## Anti-Fake-Pass Rules

Before claiming the interface "feels good", you MUST show:
- [ ] Buttons respond on `pointerDown`, not `click` (16ms faster)
- [ ] Spring physics used for interactive elements — not linear/ease duration
- [ ] Optimistic updates implemented for like/follow/save actions
- [ ] Skeletons used for content areas that load — not spinners for layout-bearing areas
- [ ] Cursor changes for disabled, draggable, text states
- [ ] Empty states have illustration + context + primary action
- [ ] At least one delight moment in success/onboarding flow

Reference: `gates/anti-fake-pass-gate.md`

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.