Pushing Backgrounds Into Text With background-clip

The background-clip property has a text keyword that paints gradients onto an element’s characters rather than its background box. That unlocks hover effects where the text color animates gradually, since you’re transitioning the background-position instead of swapping a color value.

A single clipped gradient is enough for a simple fill, but the technique gets more interesting when you stack two gradients with different clip values. One gradient clipped to text controls the foreground color on hover; the other, clipped to padding-box, paints an underline or other decorative layer beneath the text.

You can also build a scanning effect by sizing the first gradient as a thin band and sliding it over a second gradient that sets the resting text color. When the band passes over the text, the characters appear to light up.

More elaborate hover states are possible by animating different layers in opposite directions. Consider an effect where the hover-in animation slides a gradient across the text, but the mouse-out animation fades the text color back to its initial state instead of replaying the slide in reverse:

.hover {
  --c: #1095c1; /* the color */

  color: #0000;
  background: 
    linear-gradient(90deg, #fff 50%, var(--c) 0) calc(100% - var(--_p, 0%)) / 200%, 
    linear-gradient(var(--c) 0 0) 0% 100% / var(--_p, 0%) no-repeat,
    var(--_c, #0000);
  -webkit-background-clip: text, padding-box, padding-box;
          background-clip: text, padding-box, padding-box;
  transition: 0s, color .5s, background-color .5s;
}
.hover:hover {
  color: #fff;
  --_c: var(--c);
  --_p: 100%;
  transition: 0.5s, color 0s .5s, background-color 0s .5s;
}

That demo relies on three background layers — two gradients plus a background-color — and a --_c variable initialized to transparent (#0000). On hover, the variable and the text color flip to their active values. The transition delays the color and background-color changes by 0.5s so the gradient layers slide into position first. On mouse-out, the gradients snap back instantly (thanks to a 0s delay) while the color-related properties animate over time to produce the fade.

The same logic compresses into fewer declarations with the DRY switching technique, using a single variable to toggle between states:

.hover {
  --c: 16 149 193; /* the color using the RGB format */

  color: rgb(255 255 255 / var(--_i, 0));
  background:
    /* Gradient #1 */
    linear-gradient(90deg, #fff 50%, rgb(var(--c)) 0) calc(100% - var(--_i, 0) * 100%) / 200%,
    /* Gradient #2 */
    linear-gradient(rgb(var(--c)) 0 0) 0% 100% / calc(var(--_i, 0) * 100%) no-repeat,
    /* Background Color */
    rgb(var(--c)/ var(--_i, 0));
  -webkit-background-clip: text, padding-box, padding-box;
          background-clip: text, padding-box, padding-box;
  --_t: calc(var(--_i,0)*.5s);
  transition: 
    var(--_t),
    color calc(.5s - var(--_t)) var(--_t),
    background-color calc(.5s - var(--_t)) var(--_t);
}
.hover:hover {
  --_i: 1;
}

That version uses the RGB color syntax because the hover state needs an alpha channel. A custom property, --_t, stores the redundant calculation used inside the transition shorthand.

One caveat before leaning on this heavily: background-clip: text has inconsistent support. Firefox has a long history of reported bugs, and Safari also has known issues. Chrome is the only browser where this approach is reliably solid, so treat it as a progressive enhancement rather than a core interaction.

Revealing Underlines and Shapes With CSS mask

The CSS mask property accepts gradients in the same way as background, which means the layer-sliding patterns from the background techniques transfer directly. The key difference is semantic: background paints the element’s visible area, while mask determines which parts of that area remain opaque.

That separation makes it easy to animate complex underlines. A zig-zag border, for instance, is awkward to move around with background properties alone. With a mask, the border stays put in the background layer and the mask simply reveals it on hover.

The mask in that demo is two gradients:

linear-gradient(#000 0 0) content-box

The first gradient is an opaque color clipped to the content box; it keeps the text visible while hiding the bottom border. The second gradient covers the full padding box and has a width controlled by a --_p variable:

linear-gradient(#000 0 0) 0 / var(--_p, 0%) padding-box

Changing --_p on hover slides that second gradient from left to right, revealing the underline underneath:

.hover:hover {
  --_p: 100%;
  color: var(--c);
}

Because the mask and the background are configured independently, swapping the background from a zig-zag to a wavy line takes no changes to the mask at all. Different gradient configurations inside the mask itself produce distinct reveal patterns — corners wiping in, lines sweeping across, or shapes fading in — all without pseudo-elements.

Faking 3D With Gradients, clip-path, and Transforms

Real 3D rendering isn’t necessary to sell a depth effect on a single element. A convincing illusion comes from combining a bordered box, a conic gradient, and a clip-path that trims the corners.

Breakdown of the CSS hover effect in four stages.
The trick may look like we’re interacting with a 3D element, but we’re merely using 2D tactics to draw a 3D box

The setup starts with variables for the border width and a depth value:

--c: #1095c1; /* color */
--b: .1em; /* border length */
--d: 20px; /* cube depth */

Those variables become a transparent border. The top and right sides are the base width, while the bottom and left sides add the depth distance:

--_s: calc(var(--d) + var(--b));
color: var(--c);
border: solid #0000; /* fourth value sets the color's alpha */
border-width: var(--b) var(--b) var(--_s) var(--_s);

A conic gradient then paints over the whole border area:

background: conic-gradient(
  at left var(--_s) bottom var(--_s),
  #0000 90deg,var(--c) 0
 ) 
 0 100% / calc(100% - var(--b)) calc(100% - var(--b)) border-box;
Diagram of the sizing used for the hover effect.

To create shading, a second gradient of semi-transparent white layers over the first one:

conic-gradient(
  at left var(--d) bottom var(--d),
  #0000 90deg,
  rgb(255 255 255 / 0.3) 0 225deg,
  rgb(255 255 255 / 0.6) 0
) border-box
Showing the angles used to create the hover effect.

The final structural piece is a clip-path that cuts off the bottom-left corner and gives the element its extruded look:

clip-path: polygon(
  0% var(--d), 
  var(--d) 0%, 
  100% 0%, 
  100% calc(100% - var(--d)), 
  calc(100% - var(--d)) 100%, 
  0% 100%
)
Showing the coordinate points of the three-dimensional cube used in the CSS hover effect.

Animating the effect means shifting those clip-path coordinates so the visible area collapses from a 3D block into a flat rectangle. Hiding the bottom and left portions removes the depth cues:

clip-path: polygon(
  0% var(--d), /* reverses var(--d) 0% */
  var(--d) 0%, 
  100% 0%, 
  100% calc(100% - var(--d)), 
  calc(100% - var(--d)) 100%, /* reverses 100% calc(100% - var(--d)) */ 
  0% 100% /* reverses var(--d) calc(100% - var(--d)) */
)

Adding a translate transform in the opposite direction of the clipping completes the illusion. Adjusting the custom property for depth changes how far the element moves and how strongly the perspective reads. A second variant flips the movement to the top-left by altering only a few coordinate values in the same structure.

Combining the Techniques

These effects are designed to layer onto one another. A single demo can combine the background animation from the first effect, the text-shadow treatment from the second, and the 3D box technique from the third. The code looks dense at first glance, but it’s simply the three patterns stacked in one rule set.

The same combinatorial approach works with perspective to simulate rotation on an element. Applied to images, the result reads as a subtle 3D tilt without any extra markup or JavaScript.

The techniques hold up on any element — buttons, menu links, headings, or images — because the underlying properties (background, mask, clip-path, and transforms) are element-agnostic. The demos here use simple headings to keep the focus on the mechanics, but the patterns generalize cleanly to production UI components.