惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

AI
AI
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
N
News and Events Feed by Topic
O
OpenAI News
W
WeLiveSecurity
S
Secure Thoughts
www.infosecurity-magazine.com
www.infosecurity-magazine.com
H
Heimdal Security Blog
D
Darknet – Hacking Tools, Hacker News & Cyber Security
Schneier on Security
Schneier on Security
Cloudbric
Cloudbric
T
Threatpost
T
Troy Hunt's Blog
T
Tenable Blog
Hacker News - Newest:
Hacker News - Newest: "LLM"
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
V
Vulnerabilities – Threatpost
Google Online Security Blog
Google Online Security Blog
Recent Commits to openclaw:main
Recent Commits to openclaw:main
The Hacker News
The Hacker News
Know Your Adversary
Know Your Adversary
Forbes - Security
Forbes - Security
Cyberwarzone
Cyberwarzone
C
CXSECURITY Database RSS Feed - CXSecurity.com
P
Privacy & Cybersecurity Law Blog
L
Lohrmann on Cybersecurity
博客园 - 司徒正美
腾讯CDC
博客园 - 三生石上(FineUI控件)
爱范儿
爱范儿
J
Java Code Geeks
Hugging Face - Blog
Hugging Face - Blog
博客园 - 【当耐特】
M
MIT News - Artificial intelligence
T
The Blog of Author Tim Ferriss
MongoDB | Blog
MongoDB | Blog
酷 壳 – CoolShell
酷 壳 – CoolShell
GbyAI
GbyAI
D
Docker
V
Visual Studio Blog
云风的 BLOG
云风的 BLOG
I
InfoQ
WordPress大学
WordPress大学
人人都是产品经理
人人都是产品经理
A
Arctic Wolf
L
LINUX DO - 热门话题
The Cloudflare Blog
雷峰网
雷峰网
Microsoft Azure Blog
Microsoft Azure Blog
小众软件
小众软件

CSS Articles by Temani Afif

Get Ready For the Powerful CSS border-shape Property! | CSS-Tricks Let’s Play With Gap Decorations! How to Control Infinite CSS Animations (Part 2 of 2) How to Control Infinite CSS Animations (Part 1 of 2) Two Circles, One Arrow, and Anchor Positioning Making a Responsive Pyramidal Grid With Modern CSS | CSS-Tricks Making Complex CSS Shapes Using shape() | CSS-Tricks Responsive Hexagon Grid Using Modern CSS | CSS-Tricks Responsive List of Avatars Using Modern CSS (Part 2) | CSS-Tricks Responsive List of Avatars Using Modern CSS (Part 1) | CSS-Tricks Perfectly Pointed Tooltips: To The Corners Perfectly Pointed Tooltips: All Four Sides Perfectly Pointed Tooltips: A Foundation Sequential linear() Animation With N Elements | CSS-Tricks Infinite Marquee Animation using Modern CSS Better CSS Shapes Using shape() — Part 4: Close and Move | CSS-Tricks Drawing CSS Shapes using corner-shape Better CSS Shapes Using shape() — Part 3: Curves | CSS-Tricks Better CSS Shapes Using shape() — Part 2: More on Arcs | CSS-Tricks Better CSS Shapes Using shape() — Part 1: Lines and Arcs | CSS-Tricks Creating Blob Shapes using clip-path: shape() Creating Flower Shapes using clip-path: shape() Custom progress element using the attr() function A CSS-Only Star Rating Component and More! (Part 2) | CSS-Tricks A CSS-Only Star Rating Component and More! (Part 1) | CSS-Tricks How to Create Wavy Boxes Using CSS Full-Bleed Layout with Modern CSS Fancy Menu Navigation Using Anchor Positioning | CSS-Tricks How to Create a Zig-Zag Box Using CSS How to Create Zig-Zag CSS Loaders Using One Element Custom Progress Element Using Anchor Positioning & Scroll-Driven Animations How to Create Filling CSS Loaders Using One Element How to Create Curved-Edge and Rounded-Edge Shapes Using CSS CSS Tricks That Use Only One Gradient | CSS-Tricks How to create Shapes with Inner Curves using CSS Mask Custom Range Slider Using Anchor Positioning & Scroll-Driven Animations How to Get the Width/Height of Any Element in Only CSS How Keyboard Navigation Works in a CSS Game How To Create Cut-Out Shapes using The clip-path property The Modern Guide For Making CSS Shapes — Smashing Magazine css-shape.com: The Ultimate Collection of CSS-only Shapes ⚡️ Sliding 3D Image Frames In CSS — Smashing Magazine CSS Tricks To Master The clip-path Property Creating Wavy Circles with Fancy Animations in CSS Modern CSS Tooltips And Speech Bubbles (Part 2) — Smashing Magazine Do you need a Tooltip or a Speech Bubble? I have created 100 using CSS 😲 Modern CSS Tooltips And Speech Bubbles (Part 1) — Smashing Magazine Creating Flower Shapes using CSS Mask & Trigonometric Functions CSS Shapes: The Triangle The Complex But Awesome CSS border-image Property — Smashing Magazine CSS Tricks to add 3D Effects to your Text CSS Responsive Multi-Line Ribbon Shapes (Part 2) — Smashing Magazine CSS Responsive Multi-Line Ribbon Shapes (Part 1) — Smashing Magazine I have made 100+ CSS-only Ribbon Shapes | The Perfect Collection 🎀 How to create a CSS-only infinite scroll animation Re-Creating The Pop-Out Hover Effect With Modern CSS (Part 2) — Smashing Magazine Re-Creating The Pop-Out Hover Effect With Modern CSS (Part 1) — Smashing Magazine How to Create CSS Ribbon Shapes with a Single Element CSS Shapes: The Ribbon Revealing Images With CSS Mask Animations — Smashing Magazine css-loaders.com: The Biggest Collection of Loading Animations (more than 500 🤯) How To Define An Array Of Colors With CSS — Smashing Magazine CSS effects on images II Shines, Perspective, And Rotations: Fancy CSS 3D Effects For Images — Smashing Magazine What's your Lucky CSS Pattern? Check It Now! How to Add a CSS Reveal Animation to Your Images Modern Layouts using CSS Grid I created 100+ unique CSS patterns | The best collection 🤩 How to Create a Custom Range Slider Using CSS CSS Shapes: The Heart CSS Shapes: Polygon & Starburst A Text Reveal Animation using CSS CSS Tip: learn CSS the easy way! How to build a CSS-only accordion How to create Breadcrumb Navigation with CSS Different Ways to Get CSS Gradient Shadows | CSS-Tricks A CSS-only responsive Stepper component A Fancy Hover Effect For Your Avatar | CSS-Tricks How to make a zoom effect using CSS Creating a Custom Cursor using CSS CSS Infinite 3D Sliders | CSS-Tricks CSS Infinite Slider Flipping Through Polaroid Images | CSS-Tricks How to create a responsive sidebar menu using CSS CSS Infinite and Circular Rotating Image Slider | CSS-Tricks Making Static Noise From a Weird CSS Gradient Bug | CSS-Tricks CSS Grid and Custom Shapes, Part 3 | CSS-Tricks Fancy Image Decorations: Outlines and Complex Animations | CSS-Tricks Fancy Image Decorations: Masks and Advanced Hover Effects | CSS-Tricks Fancy Image Decorations: Single Element Magic | CSS-Tricks How to create an infinite image slider using CSS How to Create Wavy Shapes & Patterns in CSS | CSS-Tricks How I Made a Pure CSS Puzzle Game | CSS-Tricks How to create a Tooltip/Speech Bubble using CSS CSS Grid and Custom Shapes, Part 2 | CSS-Tricks CSS Grid and Custom Shapes, Part 1 | CSS-Tricks Zooming Images in a Grid Layout | CSS-Tricks How to create a CSS-only loader with one element Exploring CSS Grid’s Implicit Grid and Auto-Placement Powers | CSS-Tricks How to create background pattern using CSS & conic-gradient Single Element Loaders: The Bars | CSS-Tricks
How to Create a CSS-only Elastic Text Effect
Temani Afif on February 11, 2026 · 2026-02-11 · via CSS Articles by Temani Afif

Text effects where each letter animates separately are always cool and eye-catching. Such staggered animations are often achieved with JavaScript libraries, making their code a bit heavy for the relatively small design effect we’re usually shooting for. In this article, we will explore tricks to achieve a fancy text effect with just CSS and without the need of JavaScript (meaning will do the character-splitting by hand).

At the time of writing, only Chrome and Edge have full support of the features we will be using.

Hover the text in the demo below and see the magic in play:

Cool, right? We have a realistic elastic effect with nothing but CSS. It’s also flexible and easy to adjust. Before we dig into the code, let me start with an important warning. It’s a nice effect but it comes with several drawbacks.

Important Disclaimer About Accessibility

The effect we are making relies on splitting words into letters, which, in general, is a very bad idea.

A simple link with a word in it is normally like this:

<a href="#">About</a>Code language: HTML, XML (xml)

But we need to target and style individual letters, so we’ll be doing this:

<a href="#">
  <span>A</span><span>b</span><span>o</span><span>u</span><span>t</span>
</a>Code language: HTML, XML (xml)

This has accessibility drawbacks.

There is a strong temptation to use aria-* attributes to fix that up. Or that’s what I thought, anyway. I found a few online resources that recommend using a structure similar to this one:

<a href="#" aria-label="About">
  <span aria-hidden="true">
    <span>A</span><span>b</span><span>o</span><span>u</span><span>t</span>
  </span>
</a>Code language: HTML, XML (xml)

Looks good, right? No! That structure is still terrible. Actually, most of the structures you will find online are bad. I am not an expert in the field, so I asked around, and two blog posts by Adrian Roselli emerged:

I highly recommend you read them to understand why splitting words is a bad idea (and what the potential solutions might be).

So why am I making this demo anyway?

I consider it more of a CSS experiment to explore modern features. That effect probably contains many properties that you are not aware of so it’s a good opportunity to discover them. Use it for fun or within a side project, but think twice before including it anywhere in widespread use or mission critical.

Now that you are warned, let’s get started.

How Does It Work?

The idea is to use the offset() property and define a path that the letters should follow. That path will be a curve that we animate along. The offset() property is an underrated feature, but it has a lot of potential, especially when combined with modern features. I used it to create an infinite marquee animation, to perfectly position elements around a circle, to create a fancy gallery of images, and so on.

Here is a simplified example to understand the trick we will be using:

The demo above uses path() values, which comes from SVG. The three letters initially follow the first one. On hover, I switch to the second path. Thanks to the transition, we have a nice effect.

Unfortunately, using SVG is not ideal because you can only create static pixel-based paths that cannot be controlled with CSS. Instead, we are going to rely on the new shape() function, which allows us to define complex shapes (including curves) that we can easily control using CSS.

In this article, I will consider a simple usage for shape() as we only need one curve, but if you want to explore this powerful function, here are some of my previous articles:

Let’s write some code

The HTML I will work with:

<ul>
  <li>
    <a href="#"><span>A</span><span>b</span><span>o</span><span>u</span><span>t</span></a>
  </li>
  <!-- more li elements -->
</ul>Code language: HTML, XML (xml)

The CSS:

ul li a {
  display: flex;
  font-family: monospace;
}
ul li a span {
  offset-path: shape(???);
  offset-distance: ???;
}
ul li a:hover {
  offset-path: shape(???);
}Code language: CSS (css)
Nothing fancy so far

A flexbox configuration to place the letters side-by-side and a monospace font because we need all the letters to have the same width.

Next, we define the path using the following code:

offset-path: shape(from Xa Ya, curve to Xb Yb with Xc Yc / Xd Yd );Code language: CSS (css)

I am using the curve command to draw a Bezier curve from A to B, with two control points, C and D.

A diagram illustrating a curve with points labeled A, B, C, and D. Points A and B are endpoints of the curve, while C and D are control points affecting the shape of the curve.

Then I will animate the curve by adjusting the coordinates of the control points, specifically their Y value. When it is equal to the Y value of A and B, we get a straight line. When it’s bigger, we get a curve.

Illustration comparing two shapes: a curved path on the left and a straight line on the right, demonstrating the transition between the two forms.

The code of the curve will look like this:

offset-path: shape(from Xa Y, curve to Xb Y with Xc Y1 / Xd Y1);

And the one of the line will look like this:

offset-path: shape(from Xa Y, curve to Xb Y with Xc Y / Xd Y);

Notice how we are only changing the coordinate of the control points while everything else remains static.

Now let’s identify the different values. Two things to consider when working with offset:

  1. It’s defined on the child elements, but the reference box is the parent container.
  2. By default, we consider the center of the element when placing it on the path.
Illustration showing the word 'PORTFOLIO' with red arrows pointing to points A and B, indicating the horizontal center along a dashed red line.

The first letter should be at the beginning of the path, and the last one at the end, so A is at the center of the first letter and B at the center of the last one

Y = 50%
Xa = .5ch
Xb = 100% - Xa = 100% - .5ch

For C and D, we don’t have any particular rules to follow, so you can specify any value for the X coordinate. I will pick 30% for Xc, and Xd will be 100% - Xc = 70%. Feel free to adjust the values to test different variations of the curve.

Our path is now ready:

offset-path: shape(from .5ch 50%, curve to calc(100% - .5ch) 50% with 30% Y / 70% Y);

The Y value is our variable, and it will be either 50% (same as A and B) or another value, let’s define it as 50% - H. The bigger H will be, the more elasticity we will have.

Let’s try it:

It’s a mess! We didn’t define the offset-distance, which makes all the letters overlap.

Should we define a position for each letter? Nah, that’s too much work.

We are obliged to define a different position for each letter, but the good thing is that we can do it with one formula using the sibling-index()and sibling-count() functions.

The first letter should be at 0% and the last one at 100%. We have N letters, which means we need a step equal to 100%/(N - 1) to place all the letters from 0% to 100%, hence the following formula:

offset-distance: (100% * i)/(N - 1)

Where i is 0-indexed.

Written in CSS, we get:

offset-distance: calc(100%*(sibling-index() - 1)/(sibling-count() - 1))Code language: CSS (css)

Almost perfect. All the letters are correctly placed except the last one. For some reason, the 0% and 100% value are the same. offset-distance is not limited to values between 0% and 100% but can take any value (including negative ones) and there is a modulo ting that creates a kind of loop. You can travel the entire path from 0% to 100%, and starting from 100%, you return to the initial point, and you can repeat the same from 100% to 200%, and so on.

Well, it’s a bit strange and not intuitive, but the fix is simple: we change 100% with 99.9%. Hacky, but it works!

Now the placement is perfect, and on hover, you can see how the straight line becomes a curve.

The last step is to add a transition, and we are done!

Maybe not quite done, as the animation seems broken. It’s probably a bug (that I have filled here), but it’s not a big deal because I was going to refactor the code to avoid writing the same shape twice and instead animate a variable.

@property --_s {
  syntax: "<number>";
  initial-value: 0;
  inherits: true;
}
ul li a {
  --h: 20px; /* control the effect */
 
  display: flex;
  font: bold 40px monospace;
  transition: --_s .3s;
}
ul li a:hover {
  --_s: 1;
}
ul li a span {
  offset-path: 
    shape(
      from .5ch 50%, curve to calc(100% - .5ch) 50% 
      with 30% calc(50% - var(--_s)*var(--h)) / 70% calc(50% - var(--_s)*var(--h))
    );
  offset-distance: calc(99.9%*(sibling-index() - 1)/(sibling-count() - 1));
}Code language: CSS (css)

Now you have the --h variable you can adjust the control the curvature of the path and another internal variable that we animate from 0 to 1 to move from a straight line to a curve.

Tada! The animation is now perfect! But where is the elastic effect?

To get the elastic effect, we need to update the easing and rely on linear(). That’s the simplest part because I am going to use a generator to get the value.

Play with the config until you get what looks good to you. Here’s where I landed:

Now it’s good, but it can be improved if we adjust the curve slightly. Right now, the “height” of the curve is the same for all the words, but it would be ideal to have it based on the length of the word. For this, I will include sibling-count()within the formula so that the height gets bigger when the word gets wider.

Making the effect direction-aware

Our effect is good, but while we’re here, let’s go the extra mile. Let’s upgrade it and make it direction-aware. The idea is to have either a bottom curvature or a top one based on the direction of the mouse.

We already have the top curve making the variable --_s equal to 1:

ul li a:hover {
  --_s: 1;
}Code language: CSS (css)

If you change the value to -1, you get a bottom curve:

Now, we need to combine both somehow. When hovering from the top, we should get the bottom curve --_s: -1, and when hovering from the bottom, we should get the top curve --_s: 1.

First, I will add a pseudo-element of the li that fills the upper half of the element and is placed above the link:

ul li {
  position: relative;
}
ul li:after {
  content: "";
  position: absolute;
  inset: 0 0 50%;
  cursor: pointer;
}Code language: CSS (css)

From there, we can define two different selectors. When we hover the pseudo-element, it means we are also hovering the li element, so we can use:

ul li:hover a {
  --_s: -1;
}Code language: CSS (css)

When we hover the a element, we are also hovering the li element, so the above will also get triggered. but if we are hovering the pseudo-element, we are not hovering a, so we can use the following:

ul li:has(a:hover) a {
  --_s: 1;
}Code language: CSS (css)

Are you a bit lost? Don’t worry, let’s place both selectors together and see what is happening:

ul li:hover a {
  --_s: -1;
}
ul li:has(a:hover) a {
  --_s: 1;
}Code language: CSS (css)

We can either hover our element from the top (through the pseudo-element) or from the bottom (through the a element). The first case will trigger the first selector because we are also hovering li, BUT will not trigger the second one because the li “is not having its a hovered”. Now, when hovering the a element, both selectors will get triggered, and the last one will win.

We have our direction-aware feature!

It works, but it’s not as fluid as the demo I shared in the introduction. When the mouse moves the whole element, it abruptly stops one animation and triggers the other one.

To fix this, we can play with the size of the pseudo-element. When we hover it, we increase its size so it fills the entire element. This will prevent the second animation from getting triggered as we can no longer hover the a element below it. And when hovering the a element, we make the size of the pseudo-element equal to 0 hence we cannot hover it and trigger the first animation.

Much better! We make the pseudo-element transparent, and the illusion is perfect.

Conclusion

I hope you enjoyed this fun CSS experiment. I will repeat it again: think twice before using it in your project. It was a great demo to explore some modern features such as shape(), linear(), sibling-index(), etc., but it’s not a good idea to break accessibility for such an effect.