Close Menu
    DevStackTipsDevStackTips
    • Home
    • News & Updates
      1. Tech & Work
      2. View All

      A Week In The Life Of An AI-Augmented Designer

      August 22, 2025

      This week in AI updates: Gemini Code Assist Agent Mode, GitHub’s Agents panel, and more (August 22, 2025)

      August 22, 2025

      Microsoft adds Copilot-powered debugging features for .NET in Visual Studio

      August 21, 2025

      Blackstone portfolio company R Systems Acquires Novigo Solutions, Strengthening its Product Engineering and Full-Stack Agentic-AI Capabilities

      August 21, 2025

      I found the ultimate MacBook Air alternative for Windows users – and it’s priced well

      August 23, 2025

      Outdated IT help desks are holding businesses back – but there is a solution

      August 23, 2025

      Android’s latest update can force apps into dark mode – how to see it now

      August 23, 2025

      I tried the Google Pixel Watch 4 – and these key features made it feel indispensable

      August 23, 2025
    • Development
      1. Algorithms & Data Structures
      2. Artificial Intelligence
      3. Back-End Development
      4. Databases
      5. Front-End Development
      6. Libraries & Frameworks
      7. Machine Learning
      8. Security
      9. Software Engineering
      10. Tools & IDEs
      11. Web Design
      12. Web Development
      13. Web Security
      14. Programming Languages
        • PHP
        • JavaScript
      Featured

      Building Cross-Platform Alerts with Laravel’s Notification Framework

      August 23, 2025
      Recent

      Building Cross-Platform Alerts with Laravel’s Notification Framework

      August 23, 2025

      Add Notes Functionality to Eloquent Models With the Notable Package

      August 23, 2025

      How to install OpenPlatform — IoT platform

      August 22, 2025
    • Operating Systems
      1. Windows
      2. Linux
      3. macOS
      Featured

      Basics of Digital Forensics

      August 22, 2025
      Recent

      Basics of Digital Forensics

      August 22, 2025

      Top Linux Server Automation Tools: Simplifying System Administration

      August 22, 2025

      Rising from the Ashes: How AlmaLinux and Rocky Linux Redefined the Post-CentOS Landscape

      August 22, 2025
    • Learning Resources
      • Books
      • Cheatsheets
      • Tutorials & Guides
    Home»News & Updates»3D Layered Text: Interactivity and Dynamicism

    3D Layered Text: Interactivity and Dynamicism

    August 22, 2025

    In the previous two chapters, we built a layered 3D text effect, added depth and color, and then brought it to life with motion. We explored static structure, animated variations, and even some clever decoration tricks. But everything so far has been hard-coded.

    This time, we’re going dynamic.

    In this final chapter, we’re stepping into the world of interactivity by adding JavaScript into the mix. We’ll start by generating the layers programmatically, giving us more flexibility and cleaner code (and we’ll never have to copy-paste divs again). Then, we’ll add some interaction. Starting with a simple :hover effect, and ending with a fully responsive bulging text that follows your mouse in real time. Let’s go.

    3D Layered Text Article Series

    1. The Basics
    2. Motion and Variations
    3. Interactivity and Dynamicism (you are here!)

    Clean Up

    Before we jump into JavaScript, let us clean things up a bit. We will pause the animations for now and go back to the static example we wrapped up with in the first chapter. No need to touch the CSS just yet. Let us start with the HTML.

    We will strip it down to the bare essentials. All we really need is one element with the text. The class stays. It is still the right one for the job.

    <div class="layeredText">Lorem Ipsum</div>

    Scripting

    It is time. Let us start adding some JavaScript. Don’t worry, the impact on performance will be minimal. We’re only using JavaScript to set up the layers and define a few CSS variables. That’s it. All the actual style calculations still happen off the main thread, maintain high frames per second, and don’t stress the browser.

    We will begin with a simple function called generateLayers. This is where all the magic of layer generation will happen. To work its magic, the function will receive the element we want to use as the container for the layers.

    function generateLayers(element) {
      // magic goes here
    }

    To trigger the function, we will first create a small variable that holds all the elements with the layeredText class. And yes, we can have more than one on the page, as we will see later. Then, we will pass each of these elements into the generateLayers function to generate the layers.

    const layeredElements = document.querySelectorAll('.layeredText');
    
    layeredElements.forEach(generateLayers);

    Fail Safe

    Now let us dive into the generateLayers function itself and start with a small fail safe mechanism. There are situations, especially when working with frameworks or libraries that manage your DOM, where a component might get rendered more than once or a function might run multiple times. It should not happen, but we want to be ready just in case.

    So, before we do anything, we will check if the element already contains a div with the .layers class. If it does, we will simply exit the function and do nothing:

    function generateLayers(element) {
      if (element.querySelector('.layers')) return;
      
      // rest of the logic goes here
    }

    Tip: In the real world, I would treat this as a chance to catch a rendering bug. Instead of silently returning, I would probably send a message back to the dev team with the relevant data and expect the issue to be fixed.

    Counting Layers

    One last thing we need to cover before we start building the layers is the number of layers. If you remember, we have a CSS variable called --layers-count, but that will not help us here. Besides, we want this to be more dynamic than a single hardcoded value.

    Here is what we will do. We will define a constant in our JavaScript called DEFAULT_LAYERS_COUNT. As the name suggests, this will be our default value. But we will also allow each element to override it by using an attribute like data-layers="14".

    Then we will take that number and push it back into the CSS using setProperty on the parent element, since we rely on that variable in the styles.

    const DEFAULT_LAYERS_COUNT = 24;
    
    function generateLayers(element) {  
      if (element.querySelector('.layers')) return;
      
      const layersCount = element.dataset.layers || DEFAULT_LAYERS_COUNT;
      element.style.setProperty('--layers-count', layersCount);
    }

    Adding Content

    Now we have everything we need, and we can finally generate the layers. We will store the original text content in a variable. Then we will build the markup, setting the innerHTML of the parent element to match the structure we used in all the previous examples. That means a span with the original content, followed by a div with the .layers class.

    Inside that div, we will run a loop based on the number of layers, adding a new layer in each iteration:

    function generateLayers(element) {
    
      // previous code
    
      const content = element.textContent;
    
      element.innerHTML = `
        <span>${content}</span>
        <div class="layers" aria-hidden="true">
          ${Array.from({ length: layersCount}, (_, i) =>
            `<div class="layer" style="--i: ${i + 1};">${content}</div>`
          ).join('')}
        </div>
      `;
    }

    And that is it. Our 3D text is ready, and all the layers are now built entirely through JavaScript. Try playing around with it. Change the text inside the layeredText element. Add your name, your project name, your brand. Let me know how it looks.

    CodePen Embed Fallback

    Quick note: I also removed the --layers-count variable from the CSS, since it is now set dynamically with JavaScript. While I was at it, I moved the font settings out of the .layeredText element, since they should be applied globally or to a more appropriate wrapper. Just a bit of housekeeping to keep things clean.

    Normalizing Height

    Since we already added a way to set the number of layers dynamically, let us take advantage of it.

    Here is an example with three different div elements, each using a different number of layers. The first one (A) has 8 layers, the second (B) has 16, and the third (C) has 24.

    CodePen Embed Fallback

    You can clearly see the difference in height between the letters, since the total height depends on the number of layers. When it comes to color though, we used the normalized value (remember that?), so the gradient looks consistent regardless of height or layer count.

    We can just as easily normalize the total height of the layers. All we need to do is replace the --layer-offset variable with a new one called --text-height. Instead of setting the distance between each layer, we define the total height for the full stack. That lets us multiply the normalized value by --text-height, and get a consistent size no matter how many layers we have.

    .layeredText {
      --text-height: 36px;
    
      .layer {
        --n: calc(var(--i) / var(--layers-count));
    
        transform: translateZ(calc(var(--n) * var(--text-height)));
        color: hsl(200 30% calc(var(--n) * 100%));
      }
    }
    CodePen Embed Fallback

    Counter Interaction

    We are ready to start reacting to user input. But before we do anything, we need to think about the things we do not want to interact with, and that means the extra layers.

    We already handled them for screen readers using aria-hidden, but even with regular mouse interactions, these layers can get in the way. In some cases, they might block access to clickable elements underneath.

    To avoid all of that, we will add pointer-events: none; to the .layers element. This makes the layers completely ‘transparent’ to mouse clicks and hover effects.

    .layers {
      pointer-events: none;
    }

    Hovering Links

    Now we can finally start responding to user input and adding a bit of interaction. Let’s say I want to use this 3D effect on links, as a hover effect. It might be a little over the top, but we are here to have fun.

    We will start with this simple markup, just a paragraph of Lorem ipsum, but with two links inside. Each link has the .layeredText class. Right now, those links will already have depth and layers applied, but that is not what we want. We want the 3D effect to appear only on hover.

    To make that happen, we will define a new :hover block in .layeredText and move all the 3D related styles into it. That includes the color and shadow of the span, the color and translateZ of each .layer, and to make it look even better, we will also animate the opacity of the layers.

    .layeredText {
      &:hover {
        span {
          color: black;
          text-shadow: 0 0 0.1em #003;
        }
    
        .layer {
          color: hsl(200 30% calc(var(--n) * 100%));
          transform: translateZ(calc(var(--i) * var(--layer-offset) + 0.5em));
          opacity: 1;
        }
      }
    }

    Now we need to define the base appearance, the styles that apply when there is no hover. We will give the span and the layers a soft bluish color, apply a simple transition, and set the layers to be fully transparent by default.

    .layeredText {
      display: inline-block;
    
      span, .layer {
        color: hsl(200 100% 75%);
        transition: all 0.5s;
      }
    
      .layer {
        opacity: 0;
      }
    }

    Also, I added display: inline-block; to the .layeredText element. This helps prevent unwanted line breaks and allows us to apply transforms to the element, if needed. The result is a hover effect that literally makes each word pop right off the page:

    CodePen Embed Fallback

    Of course, if you are using this as a hover effect but you also have some elements that should always appear with full depth, you can easily define that in your CSS.

    For example, let us say we have both a heading and a link with the .layeredText class, but we want the heading to always show the full 3D effect. In this case, we can update the hover block selector to target both:

    .layeredText {
      &:is(h1, :hover) {
        /* full 3D styles here */
      }
    }

    This way, links will only show the effect on hover, while the heading stays bold and dimensional all the time.

    CodePen Embed Fallback

    Mouse Position

    Now we can start working with the mouse position in JavaScript. To do that, we need two things: the position of the mouse on the page, and the position of each element on the page.

    We will start with the mouse position, since that part is easy. All we need to do is add a mousemove listener, and inside it, define two CSS variables on the body: --mx for the horizontal mouse position, and --my for the vertical position.

    window.addEventListener('mousemove', e => {
      document.body.style.setProperty('--mx', e.pageX);
      document.body.style.setProperty('--my', e.pageY);
    });

    Notice that I am using e.pageX and e.pageY, not e.clientX and e.clientY. That is because I want the mouse position relative to the entire page, not just the viewport. This way it works correctly even when the page is scrolled.

    Position Elements

    Now we need to get the position of each element, specifically the top and left values. We will define a function called setRects that loops through all layeredElements, finds their position using a getBoundingClientRect function, and sets it to a couple of CSS custom properties.

    function setRects() {
      layeredElements.forEach(element => {
        const rect = element.getBoundingClientRect();
        element.style.setProperty('--top', rect.top + window.scrollY);
        element.style.setProperty('--left', rect.left + window.scrollX);
      });
    }

    Once again, I am using window.scrollX and scrollY to get the position relative to the entire page, not just the viewport.

    Keep in mind that reading layout values from the DOM can be expensive in terms of performance, so we want to do it as little as possible. We will run this function once after all the layers are in place, and again only when the page is resized, since that could change the position of the elements.

    setRects();
    window.addEventListener('resize', setRects);

    The Moving Red Dot

    That is it. We are officially done writing JavaScript for this article. At this point, we have the mouse position and the position of every element stored as CSS values.

    Great. So, what do we do with them?

    Remember the examples from the previous chapter where we used background-image? That is the key. Let us take that same idea and use a simple radial gradient, from red to white.

    .layer {
      background-clip: text;
      color: transparent;
      background-image: radial-gradient(circle at center, red 24px, white 0);
    }

    But instead of placing the center of the circle in the middle of the element, we will shift it based on the mouse position. To calculate the position of the mouse relative to the element, we simply subtract the element’s position from the mouse position. Then we multiply by 1px, since the value must be in pixels, and plug it into the at part of the gradient.

    .layer {
      background-image:
        radial-gradient(
          circle at calc((var(--mx) - var(--left)) * 1px)
                    calc((var(--my) - var(--top)) * 1px),
          red 24px,
          white 0
        );
    }

    The result is text with depth and a small red dot that follows the movement of your mouse.

    CodePen Embed Fallback

    Okay, a small red dot is not exactly mind blowing. But remember, you are not limited to that. Once you have the mouse position, you can use it to drive all sorts of dynamic effects. In just a bit, we will start building the bulging effect that kicked off this entire series, but in other cases, depending on your needs, you might want to normalize the mouse values first.

    Normalizing Mouse Position

    Just like we normalized the index of each layer earlier, we can normalize the mouse position by dividing it by the total width or height of the body. This gives us a value between 0 and 1.

    document.body.style.setProperty('--nx', e.pageX / document.body.clientWidth);
    document.body.style.setProperty('--ny', e.pageY / document.body.clientHeight);

    Normalizing the mouse values lets us work with relative positioning that is independent of screen size. This is perfect for things like adding a responsive tilt to the text based on the mouse position.

    CodePen Embed Fallback

    Bulging Text

    Now we are finally ready to build the last example. The idea is very similar to the red dot example, but instead of applying the background-image only to the top layer, we will apply it across all the layers. The color is stored in a custom variable and used to paint the gradient.

    .layer {
      --color: hsl(200 30% calc(var(--n) * 100%));
    
      color: transparent;
      background-clip: text;
      background-image:
        radial-gradient(
          circle at calc((var(--mx) - var(--left)) * 1px)
                    calc((var(--my) - var(--top)) * 1px),
                    var(--color) 24px,
                    transparent 0
        );
    }

    Now we get something similar to the red dot we saw earlier, but this time the effect spreads across all the layers.

    CodePen Embed Fallback

    Brighter Base

    We are almost there. Before we go any further with the layers, I want to make the base text look a bit weaker when the hover effect is not active. That way, we create a stronger contrast when the full effect kicks in.

    So, we will make the span text transparent and increase the opacity of its shadow:

    span {
      color: transparent;
      text-shadow: 0 0 0.1em #0004;
    }

    Keep in mind, this makes the text nearly unreadable when the hover effect is not active. That is why it is important to use a proper media query to detect whether the device supports hover. Apply this styling only when it does, and adjust it for devices that do not.

    @media (hover: hover) {
      /* when hover is supported */
    }

    Fixing Sizes

    This is it. The only thing left is to fine tune the size of the gradient for each layer. And we are done. But I do not want the bulge to have a linear shape. Using the normalized value alone will give me evenly spaced steps across all layers. That results in a shape with straight edges, like a cone.

    To get a more convex appearance, we can take advantage of the new trigonometric functions available in CSS. We will take the normalized value, multiply it by 90 degrees, and pass it through a cos() function. Just like the normalized value, the cosine will return a number between 0 and 1, but with a very different distribution. The spacing between values is non-linear, which gives us that smooth convex curve.

    --cos: calc(cos(var(--n) * 90deg));

    Now we can use this variable inside the gradient. Instead of giving the color a fixed radius, we will multiply --cos by whatever size we want the effect to be. I also added an absolute value to the calculation, so that even when --cos is very low (close to zero), the gradient still has a minimum visible size.

    And, of course, we do not want sharp, distracting edges. We want a smooth fade. So, instead of giving the transparent a hard stop point, we will give it a larger value. The difference between the var(--color) and the transparent values will control how soft the transition is.

    background-image:
      radial-gradient(
        circle at calc((var(--mx) - var(--left)) * 1px)
                  calc((var(--my) - var(--top)) * 1px),
                  var(--color) calc(var(--cos) * 36px + 24px),
                  transparent calc(var(--cos) * 72px)
      );

    And just like that, we get an interactive effect that follows the mouse and gives the impression of bulging 3D text:

    CodePen Embed Fallback

    Wrapping Up

    At this point, our 3D layered text has gone from a static stack of HTML elements to a fully interactive, mouse-responsive effect. We built dynamic layers with JavaScript, normalized depth and scale, added responsive hover effects, and used live input to shape gradients and create a bulging illusion that tracks the user’s every move.

    But more than anything, this chapter was about control. Controlling structure through code. Controlling behavior through input. And controlling perception through light, color, and movement. And we did it all with native web technologies.

    This is just the beginning. You can keep going with noise patterns, lighting, reflections, physics, or more advanced motion behaviors. Now you have the tools to explore them, and to create bold, animated, expressive typography that jumps right off the screen.

    Now go make something that moves.

    3D Layered Text Article Series

    1. The Basics
    2. Motion and Variations
    3. Interactivity and Dynamicism (you are here!)

    3D Layered Text: Interactivity and Dynamicism originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

    Source: Read More 

    Facebook Twitter Reddit Email Copy Link
    Previous Articlels-lint – fast file and directory name linter
    Next Article Il progetto Arch Linux sotto attacco!

    Related Posts

    News & Updates

    I found the ultimate MacBook Air alternative for Windows users – and it’s priced well

    August 23, 2025
    News & Updates

    Outdated IT help desks are holding businesses back – but there is a solution

    August 23, 2025
    Leave A Reply Cancel Reply

    For security, use of Google's reCAPTCHA service is required which is subject to the Google Privacy Policy and Terms of Use.

    Continue Reading

    CVE-2025-50143 – Apache HTTP Server Remote Command Execution Vulnerability

    Common Vulnerabilities and Exposures (CVEs)

    Making Animations Smarter with Data Binding: Creating a Dynamic Gold Calculator in Rive

    News & Updates

    How Infosys improved accessibility for Event Knowledge using Amazon Nova Pro, Amazon Bedrock and Amazon Elemental Media Services

    Machine Learning

    The Duolingo method: Collaboration as a core practice

    Web Development

    Highlights

    CVE-2025-8832 – Linksys WAP Stack-Based Buffer Overflow Vulnerability

    August 11, 2025

    CVE ID : CVE-2025-8832

    Published : Aug. 11, 2025, 6:15 a.m. | 18 hours, 7 minutes ago

    Description : A vulnerability was determined in Linksys RE6250, RE6300, RE6350, RE6500, RE7000 and RE9000 up to 20250801. This vulnerability affects the function setDMZ of the file /goform/setDMZ. The manipulation of the argument DMZIPAddress leads to stack-based buffer overflow. The attack can be initiated remotely. The exploit has been disclosed to the public and may be used. The vendor was contacted early about this disclosure but did not respond in any way.

    Severity: 8.8 | HIGH

    Visit the link for more details, such as CVSS details, affected products, timeline, and more…

    Community News: Latest PEAR Releases (07.28.2025)

    July 28, 2025

    CVE-2025-3278 – “UrbanGo Membership Plugin Privilege Escalation Vulnerability”

    April 22, 2025

    Tailoring foundation models for your business needs: A comprehensive guide to RAG, fine-tuning, and hybrid approaches

    May 28, 2025
    © DevStackTips 2025. All rights reserved.
    • Contact
    • Privacy Policy

    Type above and press Enter to search. Press Esc to cancel.