small hover animations

tiny movements that make a page feel alive: growing images, gentle lifts, tilts, glows, and fades.

beginner css
let's go through the little on-hover animations you see all over cute, decorative websites: an image growing slightly when you hover it, a card lifting up like it's floating, a photo tilting a bit, a soft glow appearing around something, that kind of thing. none of these need javascript, i promise. they're all plain css, and once we're through the two properties behind them (transition and transform), you'll be able to mix and match them to build almost any small hover effect you can imagine.

what we're building

i've got five small, self-contained hover effects for you, each one using the same basic recipe underneath. go ahead and hover over each image below so you can see what i mean before we get into the code:

grow demo
grow
lift demo
lift
tilt demo
tilt
glow demo
glow
fade demo
fade

step 1: the two properties i'm building everything on

before we get into any specific effect, let's understand the two css properties that are doing all the actual work here. every single example in this tutorial is just a different combination of these two, so once they click for you, the rest is easy:

  • transform: this changes how an element looks, its size, position, or angle, without affecting the layout around it. i can scale it bigger or smaller, move it up/down/left/right, rotate it, or skew it. because it doesn't affect layout, nothing else on the page shifts around when it changes, which is exactly what i want for a smooth hover effect
  • transition: this tells the browser to animate a property changing over time, instead of snapping instantly from one value to another. without a transition, a hover effect still technically works, it just happens in a single frame, which looks like a jump cut rather than an animation

the pattern is always the same: we set the property's normal (not-hovered) value on the element itself, we set the transition on that same element so the browser knows to animate it, and then we set the changed value on the :hover version of that selector. the browser handles all the in-between frames for us.

tip: always put transition on the base selector (like img), not just on img:hover. if it's only on the :hover rule, the effect will animate smoothly going in, but snap back instantly the moment you stop hovering, since the moment you're no longer hovering, that rule (and its transition) no longer applies.

step 2: growing an image on hover

let's start with the simplest effect on this page. the trick is transform: scale():

img {
  transition: transform 300ms ease;
}

img:hover {
  transform: scale(1.08);
}
  • transform: scale(1.08): this makes the image 8% bigger than its normal size in both directions at once. scale(1) would be no change at all, scale(1.5) would be 50% bigger, and scale(0.9) would shrink it down by 10%. for that subtle "grew a little" feeling rather than a jarring jump, small values like 1.05 to 1.15 tend to look best
  • transition: transform 300ms ease: the first word tells the browser which property to animate (transform), the second is how long we want the animation to take (300ms, meaning 0.3 seconds), and the third is the timing curve. we're using ease here because it starts slower, speeds up, then slows down again near the end, which reads as way more natural than a perfectly constant speed

because scale() grows the image from its center by default, the image expands equally in every direction. so if the image is sitting inside a container with no extra space around it, that growth can spill outside the container's edges and get visually clipped, or overlap neighboring elements. let's fix that in step 3.

step 3: keeping the grow effect contained

if you want the image to grow but stay neatly inside its rounded corners instead of spilling out, here's what i do: i wrap it in a container and move the overflow: hidden and rounded corners onto that wrapper instead of the image itself:

<div class="frame">
  <img src="photo.jpg">
</div>
.frame {
  display: inline-block;
  overflow: hidden;
  border-radius: 10px;
  width: 100px;
  height: 100px;
}

.frame img {
  transition: transform 400ms ease;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.frame:hover img {
  transform: scale(1.15);
}
  • overflow: hidden on .frame: this is the important line, so pay attention to this one. it tells the browser to clip anything that spills outside the wrapper's box, so when the image scales up past 100%, only the part still inside the wrapper's fixed size shows, the rest gets invisibly cut off instead of overlapping other content
  • border-radius: 10px on the wrapper, not the image: i round the wrapper instead because that means the clipping itself follows the rounded shape too, so the image's corners stay rounded even while it's scaled up and technically larger than the visible box
  • .frame:hover img: notice i'm listening for the hover on .frame (the wrapper), but the transform gets applied to the img inside it. hovering the wrapper triggers a change on a different element, and that's a really useful pattern once you start combining effects, like a caption fading in over an image at the same time the image grows
  • object-fit: cover: this makes sure the image fills the whole box without distorting its proportions, cropping the edges instead of stretching if the image's natural ratio doesn't match the box's width and height

live example: this one grows but stays clipped inside its rounded frame, go ahead and hover it and compare it to the plain "grow" demo above.

contained grow demo

step 4: lifting an element on hover

next, let's look at the "lift" effect. we use this a lot for cards, buttons, and thumbnails, it moves the element upward and adds a shadow underneath it, which tricks the eye into reading it as floating slightly off the page:

img {
  transition: transform 300ms ease, box-shadow 300ms ease;
}

img:hover {
  transform: translateY(-8px);
  box-shadow: 0 12px 18px rgba(169, 169, 169, 0.35);
}
  • translateY(-8px): this moves the element vertically. a negative value moves it up, a positive value would move it down. there's also translateX() for horizontal movement, and translate(x, y) if you want to do both at once
  • listing two properties in transition, separated by a comma, like i did with transform 300ms ease, box-shadow 300ms ease, tells the browser to animate both of them at the same time, using the same duration and curve. you can give each one a different duration too, like transform 300ms ease, box-shadow 500ms ease, if you want the shadow to keep growing slightly after the movement has already finished
  • the shadow growing bigger and softer as the element lifts (rather than just appearing instantly) is what sells the "floating away from the page" illusion for me. a shadow that stays the exact same size while the element moves away from it looks flat and doesn't read as depth

step 5: tilting on hover, and combining transforms

rotating something slightly on hover uses rotate(), but honestly this step is really about a common trap we need to watch out for: combining more than one transform on the same element.

img {
  transition: transform 300ms ease;
}

img:hover {
  transform: rotate(6deg) scale(1.05);
}
  • rotate(6deg): tilts the element clockwise by 6 degrees. a negative value, like rotate(-6deg), tilts it counter-clockwise instead
  • writing rotate(6deg) scale(1.05) in the same transform value applies both at once, so you get it tilted and slightly bigger
okay, this is the important part, so listen closely: the transform property only ever holds one value at a time. if you write two separate rules that both set transform, like a base tilt and a separate hover animation that only sets transform: translateY(-8px), the second one completely overwrites the first instead of adding to it, so the tilt just disappears the moment you hover. it's an easy mistake to make. if you want an element to always stay tilted and also lift on hover, both parts have to be written together in every rule that sets transform on it: the resting state gets transform: rotate(6deg), and the hover state gets transform: rotate(6deg) translateY(-8px), repeating the rotation both times so it never gets lost.

step 6: glowing on hover

a soft colored glow works well for anything you want to feel gentle or "magical" on hover, and for this one we'll use filter: drop-shadow() rather than the regular box-shadow:

img {
  transition: filter 300ms ease;
}

img:hover {
  filter: drop-shadow(0 0 10px rgba(255, 179, 217, 0.8));
}
  • filter: drop-shadow(0 0 10px rgba(255, 179, 217, 0.8)): the first two numbers are horizontal and vertical offset (we set both to 0 here, so the glow sits evenly all around instead of leaning to one side), the third number is how soft/spread out the blur is, and the color is a soft pink at 80% opacity
  • drop-shadow() versus box-shadow: a regular box-shadow always follows the element's rectangular box, corners included, even if the image inside has transparent parts or isn't actually rectangular. drop-shadow(), since it's a filter, follows the actual visible shape of the content instead, which matters a lot for things like a png with transparency, a rounded image, or an svg icon

step 7: fading and brightening on hover

this last one is the subtlest effect on this page: a slight opacity or brightness shift, often used to signal "this is clickable" without any movement at all.

img {
  transition: opacity 300ms ease, filter 300ms ease;
}

img:hover {
  opacity: 0.75;
  filter: brightness(1.1);
}
  • opacity: 0.75: this makes the element 75% as visible as normal, a quick way for me to signal "hovered" without any motion. 1 is fully visible, 0 is fully invisible
  • filter: brightness(1.1): lightens the image by 10%. it's useful paired with a dimming opacity, or on its own if you just want hovered elements to feel a little more lit-up

putting it together: your own hover recipe

notice that every single effect we covered above followed the exact same three-part shape:

  1. write the normal, resting styles on the element itself
  2. add a transition listing every property you're about to animate
  3. write the changed values on a :hover rule for that same element

once that clicks for you, building your own combination is just a matter of picking which properties to change: transform: scale() for size, translateX()/translateY() for movement, rotate() for tilt, box-shadow or filter: drop-shadow() for depth and glow, and opacity/filter: brightness() for lighting. mix two or three of these on the same element and you'll get something that feels custom, even though every single piece of it is one of the six examples i just walked you through.

accessibility note: let's flag this before we wrap up, some people set their operating system to reduce motion, either for comfort or because animation can trigger motion sickness or vestibular issues for them. you can respect that preference and turn these effects off for anyone who's asked for it, without removing them for everyone else:
@media (prefers-reduced-motion: reduce) {
  * {
    transition: none !important;
    animation: none !important;
  }
}

result

so now we've got six small hover effects and, more importantly, the pattern underneath all of them. the same transition + transform combo we used here for images works identically on buttons, cards, nav links, or icons. try stacking two or three of these together on your own site's thumbnails or profile picture and see what feels right for your theme.