Why I Still Hand‑Code Every Hover State

I still hand-code every hover state instead of trusting component libraries. Not because I like pain, but because that tiny interaction layer decides how the site actually feels.
Why I Still Hand‑Code Every Hover State
Photo by James Harrison / Unsplash

Hover states are where the site actually starts

I still hand-code every hover state. On every project. Buttons, cards, nav, random one-off links. All of it.

Not because I hate component libraries. I use them. I ship with them. But I do not trust them with interaction design. Especially hover.

Hover is where your interface first admits it is alive. If that moment feels generic, the whole thing feels generic. I care about that more than I care about saving 15 minutes.

The default hover look is obvious from a mile away

You know the look. Slightly darker background. Maybe a bit of box-shadow. Same timing function everywhere. Copy-pasted across an entire design system that shipped straight out of the docs.

You can spot it in three seconds. Bootstrap, Tailwind UI, Material, Ant, pick your flavor. Different themes, same energy.

I do not want my work to feel like that. If the brand voice is opinionated but the hover states feel like a SaaS template starter kit, there is a disconnect. Users cannot name the problem, but they feel it.

Hover is microcopy for your hands

Text is copy. Animation is tone. Hover is the handshake.

If I want a button to feel confident, I will use a short, punchy transition, maybe 120ms, and a subtle scale with a quick ease-out. If I want it to feel calm, I go longer. Softer curve. Less movement. More color work, less transform.

Component libraries flatten all of that. You get one motion profile for every interaction. It is like using the same intonation for a love letter and a legal threat.

The first time I regretted trusting the library

Years ago I built a signup flow for a health product. Nothing fancy visually. Clean, minimal, soft colors. We used a popular component framework with default hover states because deadlines were tight and “nobody will notice.”

People noticed. Not consciously. But support emails kept coming in with a weird pattern. Variants of “I was not sure if the button worked,” or “It felt a bit jittery.” No bugs in the code. Just bad feeling.

When I rewatched screen recordings, the problem jumped out. The default hover state snapped too hard. Background jump. Shadow on, shadow off. Combine that with a slightly delayed form submit and it looked like the click did nothing. So people clicked twice. Or bailed.

I ripped out the default styles, kept the same components, and hand-coded new hover and active states. Softer easing. Clear pressed feedback. Slight color shift on hover, then a different state for active. Same backend. Same buttons. Fewer support tickets.

That was the moment I stopped outsourcing interaction feel to libraries.

Hover is state design, not decoration

Component libraries treat hover like decoration. A bit of garnish. Sprinkle on some blue and call it a day.

I think of it as state design. I want a clear, deliberate story:

  • Resting: I exist, but I am chill.
  • Hover: I am tappable, clickable, or focusable. I am paying attention to you.
  • Active: You pressed me. I am committing.
  • Disabled / pending: I am busy or off-limits. Do not waste effort here.

The default component hover usually blurs at least two of those. Hover and active become almost identical. Disabled just means low opacity. Pending often does not exist, so people spam-click.

When I hand-code hover, I design that sequence on purpose. I decide how far we move, how fast we ease, how strong the color shift is, and what changes between hover and active. It looks like details, but it is actually feedback.

My actual process for hand-coding hover states

This is not theoretical. This is what I actually do on real projects.

1. Start in the browser, not Figma

Design tools are fine for static screenshots. They are useless for how a button feels on hover. I sketch something in Figma, sure, but I do not lock interaction decisions there.

I open the dev tools, slap on a quick base style, then live-tweak the hover. Timing, easing, color distance, shadow strength, scale, border. I nudge until my hand is happy clicking it twenty times in a row.

2. Commit to a small set of hover patterns

I am not inventing a new hover style for every element. That is chaos.

I usually end up with 3 or 4 patterns:

  • Primary actions: Slight scale up, solid color shift, strong but soft shadow, clear active pressed state that feels snappy.
  • Secondary actions: Less movement, more border and background shift, lighter shadow or no shadow. Hover is more about clarity than drama.
  • Text links: Underline behavior tuned by context. Sometimes I only shift color on hover. Sometimes I add an underline reveal animation. Depends on hierarchy.
  • Cards: I use a mix of tilt, shadow, and inner content motion, but I always keep it on a leash. Cards that fly around on hover look cheap fast.

I codify these as utilities or variants in my own layer above whatever library I am using. The components handle structure. I handle feeling.

3. Tune timing and easing per interaction type

I almost never use the framework’s default transition settings. They are usually too generic or too verbose.

For hover, I like:

  • Short: 120–180ms for small, “confident” elements.
  • Medium: 200–260ms for cards and larger patterns.
  • Ease-out curves that start fast and end soft.

For active states, I often go shorter. Pressing should feel snappier than hovering. It is a commitment, not a dance.

The key is contrast. If every hover and active state shares the same 250ms ease-in-out, the interface turns to soup. Everything feels like everything else.

4. Separate hover from focus

One thing I have learned coaching junior devs: they often treat hover and focus as the same visual state. Especially when a UI library encourages that pattern.

I think that is lazy and hurts accessibility. Hover is a pointer thing. Focus is a keyboard and accessibility thing. They are not the same.

My rule: focus always gets a clearly visible ring or outline. Full stop. Hover can be pretty. Focus must be obvious. You can layer them, but focus wins.

So I hand-code focus styles as deliberately as hover. Often paired, never identical.

Why I do this even when the client does not care

Clients usually do not say “I want handcrafted hover states.” They say “I want it to feel premium” or “I want it to feel friendly but sharp.”

Hover is where I translate that vague brief into something real. You can fake premium with a fancy hero. You cannot fake it with sloppy interactions.

Even if the client never sees the diff between the default library hover and my version, I see it. I know which one I would rather ship with my name on it.

And yes, I am Richard Lemon on the footer. So I care.

Component libraries are great scaffolding, not final texture

I am not anti-library. I like not reinventing tabs for the ninth time. I like that I can reach for a data table that already handles resizing and sorting without losing a weekend.

Where I draw the line is letting the library decide how my interface feels on contact. That is my job.

So I treat libraries as scaffolding. They give me structure, accessibility primitives, ARIA attributes, keyboard navigation. All the boring but essential stuff. Then I strip out or override their hover and active styling and rebuild that layer by hand.

Most of the time this is a thin layer of CSS on top. Sometimes it means fighting specificity or opting out of their design system token setup. It is annoying. I still do it.

The cost: yes, it is slower

Hand-coding every hover state does not scale as nicely as slapping a “variant=ghost” prop everywhere and walking away.

It means more time per component. More iteration. A bit more documentation so teammates do not accidentally reintroduce library defaults. It is a tax.

But I think the tax pays off in a few places:

  • Perceived quality: People may not praise your hover states, but they feel the overall polish.
  • Clarity: Strong interaction states reduce confusion and support noise.
  • Brand: There is a recognizable “feel” across the product that is not just color and typography.

For me that is worth an extra couple of hours per project.

The hidden benefit: I notice real-world edge cases faster

When you hand-code hover states, you naturally test them harder.

I find broken tap targets, weird scrolling behavior on mobile, z-index bugs, accidental pointer-events on pseudo-elements. All while just fiddling with hover in dev tools.

If I just accept the library defaults, I miss that phase. I trust that “it probably works in most cases” and ship. Then someone with a slightly odd setup finds the issue for me, in production.

Tactile tweaking in the browser becomes both design and QA. I like that.

How I keep the chaos under control

Hand-coding hover states can get messy if you treat every component as a snowflake. I used to do that. It sucked.

These days I keep it tight with a few constraints:

  • Tokenize transitions: I define a small set of transition tokens in CSS. Something like --hover-fast, --hover-medium, --hover-slow. Components reference these instead of raw values.
  • Document patterns, not pixels: In the project docs I describe patterns like “Primary button hover: stronger color, small scale, shadow soften” instead of “transition: 150ms, ease-out, scale(1.03).”
  • Use composition: I prefer composable classes or variants. For example, a .hover-card utility that I can attach to any card-like thing instead of copy-pasting the same set of properties twenty times.

That gives me the freedom of hand-coded interaction, but the sanity of a system.

Hover will not always be there. The thinking still matters.

Hover is already useless on a lot of mobile experiences. Touch does not hover. It commits.

I still care about hover though, because the discipline transfers. Thinking in terms of state design, feedback timing, and interaction tone carries straight over to pressed states, tapped states, and long-press behavior.

If I am deliberate about hover on desktop, I am almost always more deliberate about tap states on mobile. Same brain, different medium.

Why I am fine going against the component grain

Most teams want to standardize everything and move faster. I get it. I coach baseball, I like systems and drills. Repetition matters.

But there is a difference between standardizing the boring parts and standardizing the soul out of your product. Hover sits uncomfortably close to that line.

So I keep going against the grain. I let component libraries handle the plumbing. I keep the feel. I hand-code every hover state, even when it looks slightly obsessive from the outside.

Because when someone lands on a page I built and moves their mouse for the first time, I want that tiny, forgettable moment to carry my fingerprints.

Subscribe to my newsletter

Subscribe to my newsletter to get the latest updates and news

Member discussion