Home / Blog / From CSS to Shaders
From CSS to Shaders

From CSS to Shaders

Simon Le Marchant
Simon Le Marchant
Updated: July 23rd 2025

Picture this: you're building a modern web interface and want to create an interactive card that responds to user hover with smooth, rippling effects that follow the cursor. The kind of polished interaction where the effect feels fluid, responsive, and almost magical in its smoothness.

Your first instinct, like most frontend developers, is probably to reach for CSS and JavaScript. After all, we've gotten pretty far with CSS animations, transforms, and a bit of DOM manipulation. But what happens when you actually try to build this ripple effect using traditional web technologies?

The CSS approach

Let's think through how you might tackle animated ripples with CSS and JavaScript. The basic approach seems straightforward enough:

  1. Listen for mouse movement over the card
  2. Create a new DOM element for each ripple at the cursor position
  3. Animate it outward using CSS transforms and opacity changes
  4. Clean up the element when the animation completes
.ripple {
  position: absolute;
  border-radius: 50%;
  background: rgba(255, 255, 255, 0.3);
  transform: scale(0);
  animation: ripple-expand 0.6s ease-out forwards;
}

@keyframes ripple-expand {
  to {
    transform: scale(4);
    opacity: 0;
  }
}

This works fine for a single ripple. But the user experience you're aiming for isn't just one ripple—it's multiple overlapping ripples that create a fluid, water-like effect as the cursor moves. And this is where things start to break down.

When you move your mouse quickly across the card, you're potentially creating dozens of ripple elements in rapid succession. Each one needs to be inserted into the DOM, styled, animated, and then removed. The browser's main thread—the same thread handling your application logic, DOM updates, and layout calculations—suddenly becomes a bottleneck.

The telltale signs appear quickly: animations start to stutter, the ripples lag behind the cursor, and the overall experience feels janky. On lower-powered devices or when your application is handling other complex tasks, the effect might become so choppy that it's better to disable it entirely.

Understanding the bottleneck

The fundamental issue isn't that CSS animations are slow—they're actually quite optimized. The problem is architectural. Every time you create a new ripple element, you're asking the browser's main thread to:

  • Create and insert a new DOM node
  • Calculate its position and styling
  • Trigger layout and paint operations
  • Manage the animation lifecycle
  • Clean up when it's finished

This is happening potentially dozens of times per second, all while your application is trying to handle other user interactions, state updates, and rendering. The main thread simply wasn't designed to handle this kind of intensive, parallel animation work efficiently.

Now let’s talk GPU

This is where we start to understand why developers reach for WebGL and shaders. It's not because they want to make their lives more complicated with lower-level code—it's because they've hit a genuine performance ceiling with traditional web technologies.

Graphics Processing Units (GPUs) are fundamentally different from CPUs. While your computer's main processor excels at complex, sequential tasks, GPUs are designed for one specific job: performing simple calculations on massive amounts of data in parallel. Instead of creating and managing individual DOM elements for each ripple, we can leverage the GPU to calculate the visual effect across thousands of pixels simultaneously.

When you implement ripples using a shader, you're essentially writing a small program that runs once for every pixel on the screen. Instead of creating DOM elements, you're describing mathematically how each pixel should behave based on its distance from ripple origins, the time elapsed, and other factors.

// Fragment shader pseudo-code
vec2 pixelPosition = gl_FragCoord.xy;
float rippleEffect = 0.0;

for (int i = 0; i < numRipples; i++) {
  float distance = length(pixelPosition - ripplePositions[i]);
  float time = currentTime - rippleStartTimes[i];
  rippleEffect += calculateRipple(distance, time);
}

gl_FragColor = mix(baseColor, highlightColor, rippleEffect);

This might look intimidating if you've never seen shader code before, but the concept is elegant in its simplicity. Instead of managing dozens of animated DOM elements, you're describing the visual effect mathematically. The GPU can then calculate this formula for every pixel on the screen—potentially millions of calculations—in parallel, every frame.

It’s all just math under the hood

The performance difference is dramatic. Where the CSS approach might struggle to maintain smooth animations with just a few simultaneous ripples, a shader-based implementation can handle dozens or even hundreds of overlapping effects without breaking a sweat. The calculations are happening on dedicated graphics hardware, completely separate from your application's main thread.

But the benefits go beyond just performance. Shaders give you mathematical precision and control that's simply impossible with CSS. You can create effects that respond to mouse velocity, adjust opacity and size based on complex easing functions, or even incorporate physics-like behaviors such as wave interference patterns when multiple ripples overlap.

The ripple effect can become more than just a visual flourish—it can be a responsive, dynamic element that adapts to user behavior in subtle but meaningful ways. Maybe ripples that appear when the user moves slowly are different from those created by quick gestures. Maybe the effect responds to the pressure of a touch input, or incorporates the user's scroll velocity. These kinds of nuanced interactions are straightforward to implement in shader code but would be prohibitively complex with DOM-based approaches.

Ouch, there’s a learning curve

Now, it's important to be honest about the trade-offs. Writing shaders requires learning GLSL (OpenGL Shading Language), understanding WebGL concepts, and thinking about visual effects in mathematical terms rather than DOM manipulation. The debugging experience is different—you can't just inspect elements in your browser's dev tools. The code is less familiar and the ecosystem of learning resources, while growing, is still smaller than what exists for traditional frontend technologies.

But here's the thing: you don't need to become a graphics programming expert to start using shaders effectively. For many interactive effects, you're working with well-established patterns and mathematical formulas. The vertex shader that positions geometry can often be reused across projects, and fragment shaders for common effects like ripples, gradients, or particle systems follow predictable structures.

Modern tools and frameworks are also making shader development more accessible to web developers. Many of the low-level WebGL setup tasks can be abstracted away, letting you focus on the visual effect itself rather than GPU memory management and buffer binding. Setup is often something you’ll do once (or ask AI to do for you) and then you can focus on writing your shader code to make things pop.

Learning to work with shaders isn't just about creating flashy visual effects—it's about expanding your toolkit as a frontend developer. As web applications become more sophisticated and user expectations for polished interactions continue to rise, understanding when and how to leverage GPU capabilities becomes increasingly valuable.

Getting started

If this has sparked your curiosity, the good news is that you don't need to dive into the deep end immediately. Start by experimenting with simple fragment shaders that create gradients or basic animations. Get comfortable with the mathematical approach to describing visual effects.

The journey from CSS to shaders isn't about replacing everything you know—it's about recognizing when you've reached the limits of one tool and having the knowledge to reach for another. Sometimes that tool happens to harness the parallel processing power of graphics hardware to create interactions that would be impossible any other way.

And when you finally see that smooth, responsive ripple effect running at 120fps with dozens of overlapping animations, all while your main thread remains completely free to handle other application concerns, you'll understand why developers make this leap. It's not about showing off technical complexity—it's about creating user experiences that simply aren’t possible any other way.

If you’re looking to jump in and try building your first WebGL shader with Vue, check out my new course on Vue School and I’ll walk you through it. I’ll see you there!

Start learning Vue.js for free

Simon Le Marchant
Simon Le Marchant
Hey I'm Simon — a front-end engineer with 15+ years in leading and crafting innovative web projects. I enjoy building with Nuxt, Vue, and Electron, but also have a comprehensive full-stack background going all the way back to when Macromedia Flash and ActionScript was a thing. My core principle is to be helpful.

Comments

Latest Vue School Articles

Why AI responses Are Never the Same and How to Fix It for Developers

Why AI responses Are Never the Same and How to Fix It for Developers

Why do AI responses like ChatGPT’s vary? Learn why and how to get accurate answers for Vue.js and web development in 2025 with practical tips.
Eleftheria Batsou
Eleftheria Batsou
RAG for Vue.js and Nuxt.js: What It Is and How to Use It

RAG for Vue.js and Nuxt.js: What It Is and How to Use It

Learn RAG to build smart Vue.js apps in 2025. Add accurate AI chatbots and search with this step-by-step guide!
Eleftheria Batsou
Eleftheria Batsou
VueSchool logo

Our goal is to be the number one source of Vue.js knowledge for all skill levels. We offer the knowledge of our industry leaders through awesome video courses for a ridiculously low price.

More than 200.000 users have already joined us. You are welcome too!

Follow us on Social

© All rights reserved. Made with ❤️ by BitterBrains, Inc.