Module 7: Visual Effects, Transitions & Animations
UI Enhancements: Shadows, Glassmorphism and CSS Tooltips
How box-shadow and text-shadow are built, how to layer shadows for realistic depth, how backdrop-filter works, and how to build a tooltip that keyboard users can reach.
Shadows, blurs and small overlays are what make an interface feel layered. They are also the effects most often overdone. Each one has a specific mechanism, and knowing it lets you use less of it, and make it look better.
box-shadow#
.card {
box-shadow: 0 4px 12px rgb(0 0 0 / 0.15);
}
The values are, in order:
box-shadow: offset-x offset-y blur spread color
0 4px 12px 0 rgb(0 0 0 / 0.15)
offset-x/offset-y where the shadow is shifted
blur how soft the edge is (0 = hard)
spread (optional) grow or shrink the shadow before blurring
color nearly always a transparent black
Keywords and variations:
.inset { box-shadow: inset 0 2px 4px rgb(0 0 0 / 0.2); } /* inside the box */
.ring { box-shadow: 0 0 0 3px rgb(11 95 255 / 0.4); } /* outline-like ring */
.layers { box-shadow: 0 1px 2px rgb(0 0 0 / 0.1), 0 8px 24px rgb(0 0 0 / 0.12); }
A shadow does not affect layout, and does not respond to clicks. It follows the box's border-radius.
Realistic depth#
One big blurry shadow looks flat. In real life, a surface casts a tight dark shadow close to itself and a wide soft one further away. You can mimic it by stacking two or three shadows:
:root {
--elev-1: 0 1px 2px rgb(0 0 0 / 0.08), 0 1px 3px rgb(0 0 0 / 0.1);
--elev-2: 0 2px 4px rgb(0 0 0 / 0.08), 0 8px 16px rgb(0 0 0 / 0.1);
--elev-3: 0 4px 8px rgb(0 0 0 / 0.08), 0 16px 32px rgb(0 0 0 / 0.14);
}
.card { box-shadow: var(--elev-1); }
.card:hover { box-shadow: var(--elev-2); }
.modal { box-shadow: var(--elev-3); }
Defining an elevation scale once keeps a whole app consistent. Low opacity matters: shadows should be felt, not seen.
A box shadow follows the rectangle. For a shape with transparency, like a PNG logo or an SVG icon, use filter: drop-shadow(), which follows the actual pixels:
.logo { filter: drop-shadow(0 4px 6px rgb(0 0 0 / 0.25)); }
Large blur radii are expensive to paint, especially on elements that animate. Transitioning box-shadow on a big element can stutter. The common trick is to put the larger shadow on a pseudo-element and transition its opacity instead.
text-shadow#
.title { text-shadow: 0 2px 4px rgb(0 0 0 / 0.4); }
.glow { text-shadow: 0 0 8px #0b5fff, 0 0 20px #0b5fff; }
The format is the same as box-shadow, without spread and without inset. A subtle text shadow can help white text stay readable over a photo, though a darkening gradient on the image (Module 3) is a more reliable fix.
Glassmorphism#
The "frosted glass" look comes from one property: backdrop-filter. It applies a filter to whatever is behind the element, rather than to the element itself.
.glass {
background: rgb(255 255 255 / 0.15);
-webkit-backdrop-filter: blur(12px) saturate(140%);
backdrop-filter: blur(12px) saturate(140%);
border: 1px solid rgb(255 255 255 / 0.3);
border-radius: 16px;
box-shadow: 0 8px 32px rgb(0 0 0 / 0.2);
}
Layers, from back to front:
┌ colorful page background / image ─────────────┐
│ ┌ .glass ───────────────────────────────┐ │
│ │ 1. what is behind gets blurred │ │
│ │ 2. semi-transparent white tint on top │ │
│ │ 3. thin light border, soft shadow │ │
│ │ 4. content │ │
│ └────────────────────────────────────────┘ │
└────────────────────────────────────────────────┘
Four things make or break it:
- The element must be partly transparent. A fully opaque background hides the blurred layer.
- Something interesting must be behind it. Blurring a flat white page shows nothing. Use a gradient or an image.
- Contrast. Text over a blurred, bright background is hard to read. Check contrast against the lightest and darkest possible backdrop.
- Performance. Backdrop blur is costly, especially on large or animated areas and on low-end phones. Use it on a few small surfaces.
Provide a fallback for browsers that lack support:
.glass { background: rgb(30 41 59 / 0.9); } /* opaque fallback */
@supports (backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px)) {
.glass {
background: rgb(255 255 255 / 0.15);
-webkit-backdrop-filter: blur(12px);
backdrop-filter: blur(12px);
}
}
Tooltips with pure CSS#
A tooltip is a small box that appears next to an element. The basic technique uses a pseudo-element and the element's own data- attribute:
<button class="tip" data-tip="Save your changes" aria-describedby="save-tip">Save</button>
.tip {
position: relative;
}
.tip::after {
content: attr(data-tip);
position: absolute;
bottom: calc(100% + 8px);
left: 50%;
transform: translateX(-50%) translateY(4px);
padding: 0.35rem 0.6rem;
border-radius: 6px;
background: #111827;
color: #fff;
font-size: 0.8rem;
white-space: nowrap;
opacity: 0;
pointer-events: none;
transition: opacity 150ms ease, transform 150ms ease;
}
.tip::before { /* the little arrow */
content: "";
position: absolute;
bottom: calc(100% + 2px);
left: 50%;
transform: translateX(-50%);
border: 6px solid transparent;
border-top-color: #111827;
opacity: 0;
transition: opacity 150ms ease;
}
.tip:hover::after,
.tip:focus-visible::after {
opacity: 1;
transform: translateX(-50%) translateY(0);
}
.tip:hover::before,
.tip:focus-visible::before { opacity: 1; }
How it works: attr(data-tip) reads the attribute and uses it as the generated text. The parent is position: relative and the tooltip is absolute, so it is placed relative to the parent. The arrow is a zero-size box whose borders form a triangle. Both are hidden with opacity: 0, and pointer-events: none stops the invisible tooltip from catching the mouse.
The accessibility catch#
A tooltip that only opens on :hover is invisible to keyboard users, so the example above also uses :focus-visible. Tooltips built from generated content are also not reliably announced by screen readers. Two practical rules:
- Never put essential information in a tooltip. If someone must see it, show it on the page.
- For real interactive tooltips, use the HTML Popover API or a library, and connect the trigger to its text with
aria-describedby.
Also note the tooltip will be clipped if any ancestor has overflow: hidden, and it can run off the screen edge near viewport borders. Simple CSS tooltips cannot detect collisions, which is why libraries exist.
Common mistakes#
- Pure black, high-opacity shadows that look dirty.
- A single large shadow instead of layered ones.
- Glass effects on a flat background, where there is nothing to blur.
- Unreadable text on glass.
- Overusing
backdrop-filteracross the whole page. - Hover-only tooltips with essential content.
- Animating
box-shadowon large elements. - Forgetting the
-webkit-prefixed version for older Safari.
Practice#
- Define a three-step elevation scale with custom properties and apply it to a card, a dropdown and a modal.
- Build a frosted login card over a gradient background. Check text contrast, then add an
@supportsfallback. - Build the tooltip from this lesson, and make it appear on both hover and keyboard focus. Test with the Tab key.
- Compare a transition on
box-shadowagainst the pseudo-element opacity trick, using the Performance panel.