I am trying to add a pulsing ring that rapidly and repeatedly shrinks around a target element to draw a user's attention to that location. Not subtle, but it'll do the job. I have several places on my site I want to place it, so I want to be able to attach it to any element without significantly altering the element that I attach it to.
My current attempt uses a div with absolute positioning with a wide border and border radius of 50%:
HTML Declaration:
<a href="mysite/readme">
<div ></div>
Click Me
<a>
CSS
.attention-ring {
border-radius: 50%;
border: 10px solid red;
position: absolute;
z-index: 100;
}
The animation is achieved with jQuery. It gives it a width of 100% and quickly shrinks it using setInterval(), resetting when width reaches 0:
function animateRing() {
if ($('.attention-ring') != null) {
var ringDiameter = 100;
setInterval(function () {
if (ringDiameter > 0) {
$('.attention-ring').css({ "padding":`${ringDiameter}%`, "margin":`${-1 * ringDiameter}%`})
ringDiameter -= 1
} else {
ringDiameter = 100;
}
}, 10)
}
}
What I have does work. But there are a few problems:
The ring element at maximum diameter extends past the edge of the page. This is particularly bad for elements already close to the edge of the page. It causes intermittent blank space to appear past the edge of the page and makes the scroll-bars go haywire. Ideally I want the page to ignore that this element is going out of bound.
I currently need to place the div with the ring class directly inside elements. This means for elements, the user will have difficulty clicking on other elements on pages where this feature is active. Not ideal because I eventually want to add a way to turn this off using a different button.
How would I solve the above problems?
CodePudding user response:
This is a perfect use-case for pseudo-elements and CSS animations. In fact, you don't need any Javascript at all.
Problem #1 happens because the script resizes the actual .attention-ring
div itself. With position: absolute
this doesn't affect the size of its container, but it will still trigger overflow if it gets beyond the container's bounds. Hence the dancing scrollbars. (You'd have to set any container to position:relative; overflow:hidden
to prevent that... which could get hairy if you want to apply this in a lot of locations. Fortunately that's not necessary!)
Instead of resizing the div as an element, you can use transform: scale(<some number>)
to scale the rendered element visually. (See CSS transform property.) This takes place after the "boxes" for each element are laid out in the browser, so it doesn't cause anything to overflow.
You can then animate the transform
property in CSS with a named @keyframes rule, which gets attached to the animated element with the animation
property.
Problem #2 can be solved with two steps. First, set pointer-events: none
on the ring element. This makes clicks go "through" it, as if it's not part of the a
tag it belongs to. Then you can avoid adding a separate div by turning the ring into a ::before
pseudoelement. This way you can turn it on or off simply by adding or removing a class from the element you want to highlight.
Here's a demo of the whole thing in action:
.attention-ring::before {
display: block;
content: "";
pointer-events: none;
border-radius: 50%;
border: 10px solid red;
position: absolute;
animation: attention 1s cubic-bezier(0,0,.2,1) infinite;
}
@keyframes attention {
50%,to {
transform: scale(5); /* multiple of the initial size */
opacity: 0;
}
}
<p><a href="mysite/readme" >
Click Me
</a></p>
<p><a href="#">
Also Click Me
</a></p>
<p
Note: the display:block; content:""
on the before::
makes the ring display, otherwise the browser treats it as an empty node and won't render it.
Credit: this is partly inspired by the ping animation in Tailwind CSS, but my solution above is a bit more flexible.