A Deep Dive into radial-gradient()

radial-gradient() is more than a background trick. With a few layers, you can paint soft lights, vignettes, glare, and texture without images. By the end of this article you will build a themeable card that uses layered radial gradients for a polished lighting system, then add a glossy badge and a subtle halftone texture, all in pure CSS.

Why radial-gradient() Matters

Gradients have matured from gaudy 90s stripes to precise, controllable color fields. radial-gradient() unlocks circular and elliptical falloffs that mimic real lighting far better than linear gradients. It gives you focused hotspots, soft shadows, highlights, and depth that scale crisply on any screen because they are vector-like and procedural. You avoid network requests for background images and gain instant theming with CSS variables. You also sidestep asset export cycles when a designer nudges a highlight 20px to the left. A single background stack can replace multiple PNGs and still look sharp on 1x through 4x displays.

Prerequisites

You will follow along by building a small card component. No frameworks are required, but you should be comfortable with basic layout and a few CSS features.

  • Basic HTML
  • CSS custom properties
  • CSS pseudo-elements (::before / ::after)

Step 1: The HTML Structure

The card lives inside a centered demo wrapper. The card contains a headline, a paragraph, a call-to-action button, and a small badge. The badge will get a glossy highlight using an elliptical radial gradient. Keep the markup simple because the visuals will come from CSS.

<!-- HTML -->
<div class="demo">
  <article class="card" aria-label="Featured gradient card">
    <div class="badge" aria-hidden="true">PRO</div>
    <h2 class="card__title">radial-gradient() Lighting</h2>
    <p class="card__body">Soft hotspots, vignettes, and gloss effects rendered with layered CSS gradients. Resize to see how everything scales.</p>
    <button class="card__cta" type="button">Learn More</button>
  </article>
</div>

Step 2: The Basic CSS & Styling

Set up variables for colors and sizes, normalize the body, center the demo, and give the card its layout. At this stage the card uses a solid background. The next step will replace that background with layered radial gradients.

/* CSS */
:root {
  --bg: hsl(230 15% 12%);
  --surface: hsl(230 15% 16%);
  --text: hsl(0 0% 98%);
  --muted: hsl(0 0% 85% / 0.75);
  --accent: hsl(280 80% 65%);
  --accent-2: hsl(190 75% 60%);
  --radius: 16px;

  /* Lighting knobs */
  --spot-1-x: 20%;
  --spot-1-y: 25%;
  --spot-2-x: 85%;
  --spot-2-y: 15%;
  --vignette: hsl(230 15% 8% / 0.8);
}

*,
*::before,
*::after {
  box-sizing: border-box;
}

html, body {
  height: 100%;
}

body {
  margin: 0;
  font: 16px/1.5 system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, Cantarell, Noto Sans, sans-serif;
  color: var(--text);
  background: var(--bg);
  display: grid;
  place-items: center;
  overflow-x: hidden;
}

.demo {
  padding: 2rem;
  width: min(920px, 100%);
}

.card {
  position: relative;
  border-radius: var(--radius);
  background: var(--surface);
  padding: 2rem 2rem 2.5rem;
  max-width: 720px;
  margin-inline: auto;
  box-shadow:
    0 1px 2px hsl(0 0% 0% / 0.35),
    0 10px 30px hsl(0 0% 0% / 0.35);
  overflow: hidden;
}

.card__title {
  margin: 0 0 0.5rem;
  font-size: clamp(1.25rem, 1.1rem + 1vw, 1.75rem);
}

.card__body {
  margin: 0 0 1.25rem;
  color: var(--muted);
  max-width: 60ch;
}

.card__cta {
  appearance: none;
  border: 0;
  padding: 0.75rem 1rem;
  background: var(--accent);
  color: black;
  border-radius: 999px;
  font-weight: 600;
  cursor: pointer;
  transition: transform 120ms ease, box-shadow 200ms ease, background-color 200ms ease;
  box-shadow: 0 8px 20px hsl(280 80% 65% / 0.35);
}
.card__cta:hover {
  transform: translateY(-2px);
  box-shadow: 0 14px 30px hsl(280 80% 65% / 0.45);
}
.card__cta:active {
  transform: translateY(0);
}

Advanced Tip: Put radial-gradient positions and colors behind CSS variables (like –spot-1-x). A designer or theme switcher can adjust lighting by updating variables on :root or at component scope without touching selectors.

Step 3: Building the Layered radial-gradient() Background

Now we paint the scene with multiple radial gradients: two colored light spots, a soft vignette around the edges, and a subtle center lift. Layer order matters. The first background layer sits on top. Each layer uses the at keyword to position the gradient and size keywords to control spread.

/* CSS */
.card {
  /* previous rules… */
  background:
    /* Topmost: small accent hotspot */
    radial-gradient(
      300px 300px at var(--spot-1-x) var(--spot-1-y),
      hsl(280 80% 70% / 0.55) 0%,
      hsl(280 80% 70% / 0.0) 60%
    ),
    /* Secondary cyan hotspot */
    radial-gradient(
      280px 240px at var(--spot-2-x) var(--spot-2-y),
      hsl(190 75% 65% / 0.45) 0%,
      hsl(190 75% 65% / 0.0) 55%
    ),
    /* Center lift to avoid a dull middle */
    radial-gradient(
      600px 480px at 50% 55%,
      hsl(230 20% 18% / 0.75) 0%,
      hsl(230 20% 18% / 0.0) 70%
    ),
    /* Vignette around edges */
    radial-gradient(
      farthest-corner at 50% 50%,
      hsl(0 0% 0% / 0) 60%,
      var(--vignette) 100%
    ),
    /* Base surface color at the bottom */
    var(--surface);
}

How This Works (Code Breakdown)

The background property accepts a comma-separated stack. The first gradient in the list renders on top, so you can compose a scene. The top layer is a 300px by 300px ellipse because radial-gradient() takes either one radius (a circle) or two radii (an ellipse). This hotspot is positioned with at var(–spot-1-x) var(–spot-1-y). The first color stop starts semi-opaque and fades to transparent by 60%. That creates a sharp center that dissipates quickly.

The second layer builds a cyan hotspot with different radii (280px by 240px) to keep the scene asymmetrical. Using transparent end stops makes layers blend without abrupt edges. Place the hotspots near the top to mimic overhead lighting. You can move them in real time by changing the CSS variables in DevTools.

The third layer lifts the center. This layer is larger (600px by 480px) and has a gentle fade to maintain readable text without washing out the surface. The fourth layer adds the vignette using farthest-corner. The size keywords pick a radius relative to the box. closest-side, farthest-side, closest-corner, and farthest-corner are useful when you want gradients that adapt to card size without you recalculating pixel radii on resize.

If you are new to shape terms here, a radial gradient can render either a circle or an ellipse. For manual circles and ellipses created with classic CSS properties, see how to make a circle with CSS and how to make an ellipse with CSS. radial-gradient() gives you the same geometry but with color falloff and compositing baked in.

Step 4: Building the Glossy Badge and Halftone Texture

The badge will get a high-contrast fill and a glossy top highlight using an elliptical radial gradient that fades to transparent. We will also add a light halftone texture to the card with repeating-radial-gradient in a pseudo-element so we can toggle it independently.

/* CSS */
.badge {
  position: absolute;
  top: 1rem;
  right: 1rem;
  font: 700 0.75rem/1 system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, Cantarell, Noto Sans, sans-serif;
  letter-spacing: 0.06em;
  padding: 0.35rem 0.5rem;
  background: linear-gradient(to bottom, hsl(280 90% 60%), hsl(280 90% 55%));
  color: white;
  border-radius: 6px;
  text-shadow: 0 1px 0 hsl(0 0% 0% / 0.25);
  box-shadow: 0 6px 14px hsl(280 80% 65% / 0.4);
  overflow: hidden;
}

.badge::after {
  content: "";
  position: absolute;
  inset: 0;
  background:
    radial-gradient(80% 50% at 50% -10%,
      hsl(0 0% 100% / 0.65) 0%,
      hsl(0 0% 100% / 0.0) 60%);
  pointer-events: none;
}

/* Optional: halftone texture over the card */
.card::before {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  mix-blend-mode: soft-light;
  opacity: 0.18;
  background:
    repeating-radial-gradient(
      circle at 20% 30%,
      hsl(0 0% 100% / 0.06) 0 2px,
      hsl(0 0% 100% / 0) 2px 6px
    );
}

How This Works (Code Breakdown)

The badge uses overflow: hidden so the highlight remains clipped to its rounded shape. The ::after overlay draws a radial gradient where the center sits just above the top edge (50% -10%). That placement creates a bright arc at the top that falls off over the label, simulating a glossy bevel. Because the gradient fades to transparent, the underlying purple gradient remains visible.

The texture comes from repeating-radial-gradient(), which repeats color stops outward from the center. The pattern uses a small white ring blended with soft-light, producing a subtle lift that varies with the background behind it. Keep the opacity restrained so the texture does not interfere with text. Because it is in ::before, you can remove the texture by toggling one rule without touching the main background stack. If you want to apply a similar spotlight to a callout bubble, you can pair this lighting approach with a classic speech bubble arrow; here is how to make a tooltip shape with CSS and then layer a radial highlight behind it.

Advanced Techniques: Animations & Hover Effects

You can animate the spotlight for a subtle shimmer. Instead of animating color, which can be jarring, animate the position variables so the gradients glide. The card will idle with a gentle drift and respond to hover by nudging toward the cursor. A reduced motion query will disable the effect for users who prefer fewer animations.

/* CSS */
@keyframes light-drift {
  0% {
    --spot-1-x: 22%;
    --spot-1-y: 27%;
    --spot-2-x: 83%;
    --spot-2-y: 13%;
  }
  50% {
    --spot-1-x: 18%;
    --spot-1-y: 23%;
    --spot-2-x: 86%;
    --spot-2-y: 18%;
  }
  100% {
    --spot-1-x: 22%;
    --spot-1-y: 27%;
    --spot-2-x: 83%;
    --spot-2-y: 13%;
  }
}

.card {
  animation: light-drift 8s ease-in-out infinite;
}

/* Hover bias toward the pointer using a quick transform and stronger accent */
.card:hover {
  --spot-1-x: 26%;
  --spot-1-y: 22%;
  --spot-2-x: 88%;
  --spot-2-y: 12%;
}
.card:hover .card__cta {
  background: var(--accent-2);
  box-shadow: 0 14px 30px hsl(190 75% 60% / 0.45);
}

/* Respect motion preferences */
@media (prefers-reduced-motion: reduce) {
  .card {
    animation: none;
  }
  .card__cta {
    transition: none;
  }
}

Animating custom properties works because radial-gradient() reads the variables each frame and recomputes the background. The work is limited to paint and composite; there is no layout thrash. Use slow, long cycles for ambience. On hover, the quick variable change nudges the hotspots toward the top-right while switching the button accent to cyan for a coordinated feel.

Accessibility & Performance

Decorative gradients can improve perceived quality, but they must not threaten readability or motion comfort. The following guidelines keep the effect useful and considerate.

Accessibility

Provide enough contrast for text on top of gradients. Dark vignettes can help, but measure with a contrast checker if the content is critical. If the gradient layers carry no meaning, mark their pseudo-elements as aria-hidden=”true”. Respect motion preferences with prefers-reduced-motion as shown. If a badge label like “PRO” communicates state, duplicate that state in accessible text near the control or label, not only by color and gloss.

Performance

Modern browsers rasterize gradients efficiently, but do not stack dozens of large layers. Keep the number of backgrounds reasonable, around three to five for most components. Animating background-position or variable-driven positions is usually acceptable for small components. Avoid animating filter or box-shadow on large surfaces, which can trigger heavy repaints. If you need extreme performance on low-end devices, consider leaving the texture layer off and sticking to two or three well-tuned hotspots and a vignette. Cache expensive effects behind static screenshots only when you have measured a real problem.

Keep Painting with Gradients

You built a reusable lighting system with radial-gradient(): layered hotspots, a responsive vignette, and a glossy badge, all themeable through CSS variables. The same approach scales to dashboards, hero headers, and charts that need depth and focus without static images. With a grasp of shape, size keywords, and color stops, you now have the tools to sketch highlights as quickly as you sketch boxes.

Leave a Comment