All writing

Small micro-interactions that make buttons feel alive

Press Me

Click this button.

It works, probably, and something might have happened somewhere. But it doesn’t feel like anything happened. That’s the problem with a lot of buttons on the web. They do their job and nothing else.

You don’t need a redesign to fix that. A handful of tiny details, most of them a few lines of CSS, make a button feel alive. None of them change how it looks, so they work just as well on a flat, minimal button as they do on a gold-trimmed “Play Game” button in a fantasy game.

I made a video walking through all of these, but here they all are with buttons you can actually press.

Cursor

By default, your cursor stays as an arrow, even when it’s over a button. Set the cursor to pointer and it turns into a little hand.

Before
After
button {
    cursor: pointer;
}

Not every website does this, but it’s a clear sign that what you’re hovering over is clickable. One line of CSS for that is a bargain.

Sound

Play a quiet little click when a button’s pressed, and the action feels real. Load the sound up front with the Web Audio API, so it’s decoded and ready, and it plays the instant you tap rather than a moment later.

Before
After

Turn your sound on for this one.

const ctx = new AudioContext();
const res = await fetch('/click.mp3');
const click = await ctx.decodeAudioData(await res.arrayBuffer());

button.addEventListener('pointerdown', () => {
    const source = ctx.createBufferSource();
    source.buffer = click;
    source.connect(ctx.destination);
    source.start();
});

Keep it short, and keep it subtle. Nobody wants a button that sounds like a slot machine.

Press

Make it react when you press it. Scale it down a little bit and put a quick transition on it, so it doesn’t jump straight to the new size.

Before
After

Hold the mouse button down on each.

button {
    transition: scale 100ms ease-out;
}

button:active {
    scale: 0.97;
}

Three percent doesn’t sound like much. But now it feels like something you’re physically pushing in, and that’s the difference between a picture of a button and a button.

Hover

Most people settle for making the background a little lighter on hover. That’s fine, but think about how you can make it more interesting.

If a button has an icon, that icon can come alive a little when you hover over it. An arrow can nudge a couple of pixels in the direction it’s pointing, so it feels like you’re already acting on it before you’ve even pressed. A link that leaves your site can move its arrow up and to the right. A like button’s heart can beat once. A chat bubble can give a little wiggle, like it’s talking.

Nudge
Up and out
Beat
Wiggle
.icon {
    transition: translate 100ms ease-out;
}

.deploy:hover .icon {
    translate: 2px 0;
}

.docs:hover .icon {
    translate: 1px -1px;
}

.like:hover .icon {
    animation: beat 300ms ease-out;
}

.chat:hover .icon {
    transform-origin: 50% 85%;
    animation: wiggle 400ms ease-out;
}

@keyframes beat {
    50% { scale: 1.3; }
}

@keyframes wiggle {
    25% { rotate: -12deg; }
    50% { rotate: 10deg; }
    75% { rotate: -5deg; }
}

It’s a micro-interaction, not the main show. Keep it small and subtle. If anyone consciously notices the arrow moving, it’s moving too far.

Hit area

On phones and tablets, you press things with your finger instead of a mouse pointer, and that makes really small buttons hard to tap. Even if you can hit them consistently, fingers come in all shapes and sizes, and you don’t know whether someone else can just because you can.

If someone fat-fingers it and misses by a few pixels, nothing happens. The app just feels unresponsive.

Apple’s Human Interface Guidelines recommend that anything you can tap is at least 44 by 44 points. So does that mean making all your buttons bigger, and breaking your design? Not necessarily!

You can add a pseudo-element that makes the hit area stick out past the edges of the button. Give it a minimum width and height of 44 pixels and centre it, and small buttons get a comfortable target while bigger buttons stay exactly as they are.

Tiny button 0 taps
Normal button 0 taps

Try clicking just outside the tiny button. It still counts. The normal button’s hit area is already bigger than 44 pixels, so it doesn’t change at all.

@media (pointer: coarse) {
    button {
        position: relative;
    }

    button::before {
        content: '';
        position: absolute;
        top: 50%;
        left: 50%;
        translate: -50% -50%;
        width: 100%;
        height: 100%;
        min-width: 44px;
        min-height: 44px;
    }
}

The pointer: coarse media query means it only kicks in when someone’s using something imprecise, like a finger on a touchscreen, so it doesn’t affect desktop users with a mouse. I’ve left it on in the demo above so you can see it with one.

Haptics

Phones can do something else, too. Haptics! One call to navigator.vibrate() gives a tiny vibration you can physically feel, which tricks your brain into thinking you pressed a real, physical button.

With haptics

Try this one on an Android phone.

button.addEventListener('pointerdown', () => {
    if ('vibrate' in navigator) {
        navigator.vibrate(10);
    }
});

This works great on Android. Apple doesn’t want us to have any fun on the web though, so it doesn’t work on an iPhone.

State

When a button’s clicked, show what it’s doing instead of giving people zero feedback. While the work happens in the background, change the label to “Submitting”, and when it comes back, change it to “Submitted”.

If the new text changes the button’s size, transition the width too, so it doesn’t just jump and shift everything around it.

Even better, morph the text. Letters that stay the same slide into place, and only the new ones fade in. Torph does exactly that for you. You can do the same with the icon: morph the arrow into a spinner while the work is happening, then into a tick when it comes back successfully. And you could even play a little success sound when it’s done.

Before
After
import { TextMorph } from 'torph';

const morph = new TextMorph({ element: label });

const from = button.offsetWidth;
morph.update('Submitting');
const to = button.offsetWidth;

button.animate(
    { width: [`${from}px`, `${to}px`] },
    { duration: 200, easing: 'ease-out' },
);

For the icon, flubber interpolates between any two SVG shapes, so you can morph from one icon to another frame by frame:

import { interpolate } from 'flubber';

const morph = interpolate(arrow, spinner);
const start = performance.now();

requestAnimationFrame(function frame(now) {
    const t = Math.min((now - start) / 300, 1);
    icon.setAttribute('d', morph(t));
    if (t < 1) requestAnimationFrame(frame);
});

Not every button

You don’t have to do all of this to every single button in your app. That would be too much, and a settings page where every toggle chirps and wiggles is exhausting.

Save it for the buttons people press less often but that have a big impact. The checkout button. The deploy button. The one that sends the email you’ve spent an hour writing. That’s where a little extra care makes the moment feel satisfying instead of nerve-wracking.

All of it

Pick the buttons that matter, give them a little life, and make people want to press them.