CSS button hover effects: four to copy, and the rules

Four hover effects in plain CSS to copy, a fill that slides in, an underline that draws itself, a card that lifts and a 3D push button, with the rules that make any hover effect feel smooth. Put the transition on the element, animate transform, give keyboards the same with :focus-visible, and keep phones from sticking.

Short answer: Put transition on the element itself, not on :hover, so it animates both in and out. Animate transform, opacity and colours rather than width, top or margin, so it stays smooth. Repeat every :hover rule for :focus-visible, so keyboard users get the same effect. Keep it under half a second, and wrap it in prefers-reduced-motion for people who turn motion off.

A good hover effect tells you "this is clickable" a moment before you click. A bad one jumps, flickers, or only works with a mouse. The four below are plain CSS, exported from Motion Studio, and they follow the same rules.

A fill that slides in

The button's background colour sweeps in from the left on hover and leaves to the right. It is a pseudo-element scaled from 0 to 1, so only transform animates:

<button class="fill-btn" type="button">Get started</button>
.fill-btn {
  position: relative;
  z-index: 0;
  overflow: hidden;
  padding: .85em 1.6em;
  border: 2px solid #2563eb;
  border-radius: .7em;
  background: transparent;
  color: #2563eb;
  font-family: inherit;
  font-size: 16px;
  font-weight: 600;
  line-height: 1;
  cursor: pointer;
  transition: color 0.45s cubic-bezier(.65, 0, .35, 1);
}

.fill-btn::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  background: #2563eb;
  transform: scaleX(0);
  transform-origin: right;
  transition: transform 0.45s cubic-bezier(.65, 0, .35, 1);
}

.fill-btn:hover, .fill-btn:focus-visible {
  color: #ffffff;
}

.fill-btn:hover::before, .fill-btn:focus-visible::before {
  transform: scaleX(1);
  transform-origin: left;
}

The trick is transform-origin: it is left while hovered and right at rest, so the fill grows from the left and shrinks towards the right, sweeping through instead of bouncing back.

An underline that draws itself

A link whose underline grows from left to right. The line is a background gradient 2px high, so the text never moves:

<a class="underline-link" href="#">Read the story</a>
.underline-link {
  padding-bottom: 3px;
  color: #1d1a26;
  font-family: inherit;
  font-size: 18px;
  font-weight: 600;
  text-decoration: none;
  background: linear-gradient(#ff5fa8, #ff5fa8) no-repeat right bottom/0 2px;
  transition: background-size 0.4s cubic-bezier(.65, 0, .35, 1);
}

.underline-link:hover, .underline-link:focus-visible {
  background-size: 100% 2px;
  background-position-x: left;
}

Unlike a pseudo-element, a background follows the text when a long link wraps onto a second line.

A card that lifts

A card rises 8px and its shadow grows, as if it came towards you:

<div class="lift-card" tabindex="0">
  <b>Lift on hover</b>
  <span>Cards rise to meet the pointer.</span>
</div>
.lift-card {
  display: grid;
  gap: 6px;
  width: 180px;
  padding: 18px;
  border: 1px solid #1d1a2614;
  border-radius: 16px;
  background: #ffffff;
  color: #1d1a26;
  font-family: inherit;
  box-shadow: 0 2px 6px rgba(40, 32, 70, 0.08);
  transition: transform 0.35s cubic-bezier(.2, .8, .2, 1), box-shadow 0.35s cubic-bezier(.2, .8, .2, 1), border-color 0.35s cubic-bezier(.2, .8, .2, 1);
}

.lift-card b {
  font-size: 16px;
}

.lift-card span {
  font-size: 13px;
  line-height: 1.4;
  opacity: .65;
}

.lift-card:hover, .lift-card:focus-visible {
  transform: translateY(-8px);
  border-color: #3d8bff8c;
  box-shadow: 0 20px 32px -8px #3d8bff73;
}

When the whole card is a link, make it an <a> and drop tabindex; tabindex="0" is only for a card that is not a link but should still show its focus.

A 3D push button

A button with a solid shadow underneath that rises a little on hover and sinks on click:

<button class="press-btn" type="button">Push me</button>
.press-btn {
  padding: .8em 1.6em;
  border: 0;
  border-radius: .7em;
  background: #ff5fa8;
  color: #fff;
  font-family: inherit;
  font-size: 16px;
  font-weight: 600;
  line-height: 1;
  cursor: pointer;
  box-shadow: 0 6px 0 #a63e6d, 0 10px 18px -6px #ff5fa899;
  transition: transform 0.15s ease-out, box-shadow 0.15s ease-out;
}

.press-btn:hover {
  transform: translateY(-2px);
  box-shadow: 0 8px 0 #a63e6d, 0 14px 22px -6px #ff5fa899;
}

.press-btn:active {
  transform: translateY(6px);
  box-shadow: 0 0 0 #a63e6d, 0 2px 6px -2px #ff5fa899;
}

On :active the button moves down exactly as far as its 6px shadow and the shadow goes to 0, so it looks pressed into the page. Short and ease-out is right here: a click has to feel instant. A <button> already shows the browser's focus ring, so this one needs no :focus-visible rule of its own.

Why does my hover effect only animate one way?

Because the transition is on :hover. Then it exists only while hovered: the effect animates in and snaps back out. Put transition on the element itself, as all four above do, and it applies in both directions.

Why is my hover animation jerky?

Animating width, height, top, left, margin or padding makes the browser lay out the page again on every frame. transform and opacity do not, so they stay smooth even on a slow phone. The fill above uses scaleX instead of growing a width for exactly this reason, and the card uses translateY instead of margin-top.

Do hover effects work on phones?

There is no hover on a touch screen: a tap usually fires the hover styles and the click together, and on some phones the hover look stays until you tap somewhere else. If a hover effect matters only for a mouse, wrap it in @media (hover: hover) { ... }, which applies only when the main pointer can hover. And never hide something important behind a hover: a phone user cannot reach it.

What about people who turn animations off?

Add this to each effect (here for the fill button); the hover still changes colour, it just stops moving:

@media (prefers-reduced-motion: reduce) {
  .fill-btn, .fill-btn::before {
    transition: none;
  }
}

prefers-reduced-motion explained covers the rule, and ease-in vs ease-out which easing suits which movement.

Where can you check this?

Can you try them with your own colours first?

Motion Studio has these four among its buttons and hover effects (shine, border draw, gradient border, jelly, neon, rolling label and more), each with its colours, size, duration and easing as settings. It exports HTML and CSS, React, Vue or Svelte with the reduced-motion rule already included, or a GIF or video of the effect.

The tool

Do it in your browser.

Design

Motion Studio

150 ready-made motion graphics, from loaders and text effects to animated backgrounds, hover effects, charts and cards that follow the pointer, all playing at once: change colours, size, speed and easing, then take HTML and CSS, React, Vue or Svelte, or a GIF, a video or an animated SVG.

Keep reading

More from the blog.

Every post