Tag: Animations

Fancy Image Decorations: Outlines and Complex Animations

We’ve spent the last two articles in this three-part series playing with gradients to make really neat image decorations using nothing but the <img> element. In this third and final piece, we are going to explore more techniques using the CSS outline property. That might sound odd because we generally use outline to draw a simple line around an element — sorta like border but it can only draw all four sides at once and is not part of the Box Model.

We can do more with it, though, and that’s what I want to experiment with in this article.

Fancy Image Decorations series

Let’s start with our first example — an overlay that disappears on hover with a cool animation:

We could accomplish this by adding an extra element over the image, but that’s what we’re challenging ourselves not to do in this series. Instead, we can reach for the CSS outline property and leverage that it can have a negative offset and is able to overlap its element.

img {   --s: 250px; /* the size of the image */   --b: 8px;   /* the border thickness*/   --g: 14px;  /* the gap */   --c: #4ECDC4;    width: var(--s);   aspect-ratio: 1;   outline: calc(var(--s) / 2) solid #0009;   outline-offset: calc(var(--s) / -2);   cursor: pointer;   transition: 0.3s; } img:hover {   outline: var(--b) solid var(--c);   outline-offset: var(--g); }

The trick is to create an outline that’s as thick as half the image size, then offset it by half the image size with a negative value. Add in some semi-transparency with the color and we have our overlay!

Diagram showing the size of the outline sround the image and how it covers the image on hover.

The rest is what happens on :hover. We update the outline and the transition between both outlines creates the cool hover effect. The same technique can also be used to create a fading effect where we don’t move the outline but make it transparent.

Instead of using half the image size in this one, I am using a very big outline thickness value (100vmax) while applying a CSS mask. With this, there’s no longer a need to know the image size — it trick works at all sizes!

Diagram showing how adding a mask clips the extra outline around the image.

You may face issues using 100vmax as a big value in Safari. If it’s the case, consider the previous trick where you replace the 100vmax with half the image size.

We can take things even further! For example, instead of simply clipping the extra outline, we can create shapes and apply a fancy reveal animation.

Cool right? The outline is what creates the yellow overlay. The clip-path clips the extra outline to get the star shape. Then, on hover, we make the color transparent.

Oh, you want hearts instead? We can certainly do that!

Imagine all the possible combinations we can create. All we have to do is to draw a shape with a CSS mask and/or clip-path and combine it with the outline trick. One solution, infinite possibilities!

And, yes, we can definitely animate this as well. Let’s not forget that clip-path is animatable and mask relies on gradients — something we covered in super great detail in the first two articles of this series.

I know, the animation is a bit glitchy. This is more of a demo to illustrate the idea rather than the “final product” to be used in a production site. We’d wanna optimize things for a more natural transition.

Here is a demo that uses mask instead. It’s the one I teased you with at the end of the last article:

Did you know that the outline property was capable of so much awesomeness? Add it to your toolbox for fancy image decorations!

Combine all the things!

Now that we have learned many tricks using gradients, masks, clipping, and outline, it’s time for the grand finale. Let’s cap off this series by combine all that we have learned the past few weeks to showcase not only the techniques, but demonstrate just how flexible and modular these approaches are.

If you were seeing these demos for the first time, you might assume that there’s a bunch of extra divs wrappers and pseudo-elements being used to pull them off. But everything is happening directly on the <img> element. It’s the only selector we need to get these advanced shapes and effects!

Wrapping up

Well, geez, thanks for hanging out with me in this three-part series the past few weeks. We explored a slew of different techniques that turn simple images into something eye-catching and interactive. Will you use everything we covered? Certainly not! But my hope is that this has been a good exercise for you to dig into advanced uses of CSS features, like gradients, mask, clip-path, and outline.

And we did everything with just one <img> element! No extra div wrappers and pseudo-elements. Sure, it’s a constraint we put on ourselves, but it also pushed us to explore CSS and try to find innovative solutions to common use cases. So, before pumping extra markup into your HTML, think about whether CSS is already capable of handling the task.

Fancy Image Decorations series


Fancy Image Decorations: Outlines and Complex Animations originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

CSS-Tricks

, , , , ,

Responsive Animations for Every Screen Size and Device

Before I career jumped into development, I did a bunch of motion graphics work in After Effects. But even with that background, I still found animating on the web pretty baffling.

Video graphics are designed within a specific ratio and then exported out. Done! But there aren’t any “export settings” on the web. We just push the code out into the world and our animations have to adapt to whatever device they land on.

So let’s talk responsive animation! How do we best approach animating on the wild wild web? We’re going to cover some general approaches, some GSAP-specific tips and some motion principles. Let’s start off with some framing…

How will this animation be used?

Zach Saucier’s article on responsive animation recommends taking a step back to think about the final result before jumping into code.

Will the animation be a module that is repeated across multiple parts of your application? Does it need to scale at all? Keeping this in mind can help determine the method in which an animation should be scaled and keep you from wasting effort.

This is great advice. A huge part of designing responsive animation is knowing if and how that animation needs to scale, and then choosing the right approach from the start.

Most animations fall into the following categories:

  • Fixed: Animations for things like icons or loaders that retain the same size and aspect ratio across all devices. Nothing to worry about here! Hard-code some pixel values in there and get on with your day.
  • Fluid: Animations that need to adapt fluidly across different devices. Most layout animations fall into this category.
  • Targeted: Animations that are specific to a certain device or screen size, or change substantially at a certain breakpoint, such as desktop-only animations or interactions that rely on device-specific interaction, like touch or hover.

Fluid and targeted animations require different ways of thinking and solutions. Let’s take a look…

Fluid animation

As Andy Bell says: Be the browser’s mentor, not its micromanager — give the browser some solid rules and hints, then let it make the right decisions for the people that visit it. (Here are the slides from that presentation.)

Fluid animation is all about letting the browser do the hard work. A lot of animations can easily adjust to different contexts just by using the right units from the start. If you resize this pen you can see that the animation using viewport units scales fluidly as the browser adjusts:

The purple box even changes width at different breakpoints, but as we’re using percentages to move it, the animation scales along with it too.

Animating layout properties like left and top can cause layout reflows and jittery ‘janky’ animation, so where possible stick to transforms and opacity.

We’re not just limited to these units though — let’s take a look at some other possibilities.

SVG units

One of the things I love about working with SVG is that we can use SVG user units for animation which are responsive out of the box. The clue’s in the name really — Scalable Vector Graphic. In SVG-land, all elements are plotted at specific coordinates. SVG space is like an infinite bit of graph paper where we can arrange elements. The viewBox defines the dimensions of the graph paper we can see.

viewBox="0 0 100 50”

In this next demo, our SVG viewBox is 100 units wide and 50 units tall. This means if we animate the element by 100 units along the x-axis, it will always move by the entire width of its parent SVG, no matter how big or small that SVG is! Give the demo a resize to see.

Animating a child element based on a parent container’s width is a little tricker in HTML-land. Up until now, we’ve had to grab the parent’s width with JavaScript, which is easy enough when you’re animating from a transformed position, but a little fiddlier when you’re animating to somewhere as you can see in the following demo. If your end-point is a transformed position and you resize the screen, you’ll have to manually adjust that position. Messy… 🤔

If you do adjust values on resize, remember to debounce, or even fire the function after the browser is finished resizing. Resize listeners fire a ton of events every second, so updating properties on each event is a lot of work for the browser.

But, this animation speed-bump is soon going to be a thing of the past! Drum roll please… 🥁

Container Units! Lovely stuff. At the time I’m writing this, they only work in Chrome and Safari — but maybe by the time you read this, we’ll have Firefox too. Check them out in action in this next demo. Look at those little lads go! Isn’t that exciting, animation that’s relative to the parent elements!

This browser support data is from Caniuse, which has more detail. A number indicates that browser supports the feature at that version and up.

Desktop

Chrome Firefox IE Edge Safari
105 No No 105 16.0

Mobile / Tablet

Android Chrome Android Firefox Android iOS Safari
106 No 106 16.0

Fluid layout transitions with FLIP

As we mentioned earlier, in SVG-land every element is neatly placed on one grid and really easy to move around responsively. Over in HTML-land it’s much more complex. In order to build responsive layouts, we make use of a bunch of different positioning methods and layout systems. One of the main difficulties of animating on the web is that a lot of changes to layout are impossible to animate. Maybe an element needs to move from position relative to fixed, or some children of a flex container need to be smoothly shuffled around the viewport. Maybe an element even needs to be re-parented and moved to an entirely new position in the DOM.

Tricky, huh?

Well. The FLIP technique is here to save the day; it allows us to easily animate these impossible things. The basic premise is:

  • First: Grab the initial position of the elements involved in the transition.
  • Last: Move the elements and grab the final position.
  • Invert: Work out the changes between the first and last state and apply transforms to invert the elements back to their original position. This makes it look like the elements are still in the first position but they’re actually not.
  • Play: Remove the inverted transforms and animate to their faked first state to the last state.

Here’s a demo using GSAP’s FLIP plugin which does all the heavy lifting for you!

If you want to understand a little more about the vanilla implementation, head over to Paul Lewis’s blog post — he’s the brain behind the FLIP technique.

Fluidly scaling SVG

You got me… this isn’t really an animation tip. But setting the stage correctly is imperative for good animation! SVG scales super nicely by default, but we can control how it scales even further with preserveAspectRatio, which is mega handy when the SVG element’s aspect ratio and the viewBox aspect ratio are different. It works much in the same way as the background-position and background-size properties in CSS. The declaration is made up of an alignment value (background-position) and a Meet or Slice reference (background-size).

As for those Meet and Slice references — slice is like background size: cover, and meet is like background-size: contain.

  • preserveAspectRatio="MidYMax slice" — Align to the middle of the x-axis, the bottom of the y-axis, and scale up to cover the entire viewport.
  • preserveAspectRatio="MinYMin meet" — Align to the left of the x-axis, the top of the y-axis, and scale up while keeping the entire viewBox visible.

Tom Miller takes this a step further by using overflow: visible in CSS and a containing element to reveal “stage left” and “stage right” while keeping the height restricted:

For responsive SVG animations, it can be handy to make use of the SVG viewbox to create a view that crops and scales beneath a certain browser width, while also revealing more of the SVG animation to the right and left when the browser is wider than that threshold. We can achieve this by adding overflow visible on the SVG and teaming it up with a max-height wrapper to prevent the SVG from scaling too much vertically.

Fluidly scaling canvas

Canvas is much more performant for complex animations with lots of moving parts than animating SVG or HTML DOM, but it’s inherently more complex too. You have to work for those performance gains! Unlike SVG that has lovely responsive units and scaling out of the box, <canvas> has to be bossed around and micromanaged a bit.

I like setting up my <canvas> so that it works much in the same way as SVG (I may be biased) with a lovely unit system to work within and a fixed aspect ratio. <canvas> also needs to be redrawn every time something changes, so remember to delay the redraw until the browser is finished resizing, or debounce!

George Francis also put together this lovely little library which allows you to define a Canvas viewBox attribute and preserveAspectRatio — exactly like SVG!

Targeted animation

You may sometimes need to take a less fluid and more directed approach to your animation. Mobile devices have a lot less real estate, and less animation-juice performance-wise than a desktop machine. So it makes sense to serve reduced animation to mobile users, potentially even no animation:

Sometimes the best responsive animation for mobile is no animation at all! For mobile UX, prioritize letting the user quickly consume content versus waiting for animations to finish. Mobile animations should enhance content, navigation, and interactions rather than delay it. Eric van Holtz

In order to do this, we can make use of media queries to target specific viewport sizes just like we do when we’re styling with CSS! Here’s a simple demo showing a CSS animation being handled using media queries and a GSAP animation being handled with gsap.matchMedia():

The simplicity of this demo is hiding a bunch of magic! JavaScript animations require a bit more setup and clean-up in order to correctly work at only one specific screen size. I’ve seen horrors in the past where people have just hidden the animation from view in CSS with opacity: 0, but the animation’s still chugging away in the background using up resources. 😱

If the screen size doesn’t match anymore, the animation needs to be killed and released for garbage collection, and the elements affected by the animation need to be cleared of any motion-introduced inline styles in order to prevent conflicts with other styling. Up until gsap.matchMedia(), this was a fiddly process. We had to keep track of each animation and manage all this manually.

gsap.matchMedia() instead lets you easily tuck your animation code into a function that only executes when a particular media query matches. Then, when it no longer matches, all the GSAP animations and ScrollTriggers in that function get reverted automatically. The media query that the animations are popped into does all the hard work for you. It’s in GSAP 3.11.0 and it’s a game changer!

We aren’t just constrained to screen sizes either. There are a ton of media features out there to hook into!

(prefers-reduced-motion) /* find out if the user would prefer less animation */  (orientation: portrait) /* check the user's device orientation */  (max-resolution: 300dpi) /* check the pixel density of the device */

In the following demo we’ve added a check for prefers-reduced-motion so that any users who find animation disorienting won’t be bothered by things whizzing around.

And check out Tom Miller’s other fun demo where he’s using the device’s aspect ratio to adjust the animation:

Thinking outside of the box, beyond screen sizes

There’s more to thinking about responsive animation than just screen sizes. Different devices allow for different interactions, and it’s easy to get in a bit of a tangle when you don’t consider that. If you’re creating hover states in CSS, you can use the hover media feature to test whether the user’s primary input mechanism can hover over elements.

@media (hover: hover) {  /* CSS hover state here */ }

Some advice from Jake Whitely:

A lot of the time we base our animations on browser width, making the naive assumption that desktop users want hover states. I’ve personally had a lot of issues in the past where I would switch to desktop layout >1024px, but might do touch detection in JS – leading to a mismatch where the layout was for desktops, but the JS was for mobiles. These days I lean on hover and pointer to ensure parity and handle ipad Pros or windows surfaces (which can change the pointer type depending on whether the cover is down or not)

/* any touch device: */ (hover: none) and (pointer: coarse) /* iPad Pro */ (hover: none) and (pointer: coarse) and (min-width: 1024px)

I’ll then marry up my CSS layout queries and my JavaScript queries so I’m considering the input device as the primary factor supported by width, rather than the opposite.

ScrollTrigger tips

If you’re using GSAP’s ScrollTrigger plugin, there’s a handy little utility you can hook into to easily discern the touch capabilities of the device: ScrollTrigger.isTouch.

  • 0no touch (pointer/mouse only)
  • 1touch-only device (like a phone)
  • 2 – device can accept touch input and mouse/pointer (like Windows tablets)
if (ScrollTrigger.isTouch) {   // any touch-capable device... }  // or get more specific:  if (ScrollTrigger.isTouch === 1) {   // touch-only device }

Another tip for responsive scroll-triggered animation…

The following demo below is moving an image gallery horizontally, but the width changes depending on screen size. If you resize the screen when you’re halfway through a scrubbed animation, you can end up with broken animations and stale values. This is a common speedbump, but one that’s easily solved! Pop the calculation that’s dependent on screen size into a functional value and set invalidateOnRefresh:true. That way, ScrollTrigger will re-calculate that value for you when the browser resizes.

Bonus GSAP nerd tip!

On mobile devices, the browser address bar usually shows and hides as you scroll. This counts as a resize event and will fire off a ScrollTrigger.refresh(). This might not be ideal as it can cause jumps in your animation. GSAP 3.10 added ignoreMobileResize. It doesn’t affect how the browser bar behaves, but it prevents ScrollTrigger.refresh() from firing for small vertical resizes on touch-only devices.

ScrollTrigger.config({   ignoreMobileResize: true });

Motion principles

I thought I’d leave you with some best practices to consider when working with motion on the web.

Distance and easing

A small but important thing that’s easy to forget with responsive animation is the relationship between speed, momentum, and distance! Good animation should mimic the real world to feel believable, and it takes a longer in the real world to cover a larger distance. Pay attention to the distance your animation is traveling, and make sure that the duration and easing used makes sense in context with other animations.

You can also often apply more dramatic easing to elements with further to travel to show the increased momentum:

For certain use cases it may be helpful to adjust the duration more dynamically based on screen width. In this next demo we’re making use of gsap.utils to clamp the value we get back from the current window.innerWidth into a reasonable range, then we’re mapping that number to a duration.

Spacing and quantity

Another thing to keep in mind is the spacing and quantity of elements at different screen sizes. Quoting Steven Shaw:

If you have some kind of environmental animation (parallax, clouds, trees, confetti, decorations, etc) that are spaced around the window, make sure that they scale and/or adjust the quantity depending on screen size. Large screens probably need more elements spread throughout, while small screens only need a few for the same effect.

I love how Opher Vishnia thinks about animation as a stage. Adding and removing elements doesn’t just have to be a formality, it can be part of the overall choreography.

When designing responsive animations, the challenge is not how to cram the same content into the viewport so that it “fits”, but rather how to curate the set of existing content so it communicates the same intention. That means making a conscious choice of which pieces content to add, and which to remove. Usually in the world of animation things don’t just pop in or out of the frame. It makes sense to think of elements as entering or exiting the “stage”, animating that transition in a way that makes visual and thematic sense.

And that’s the lot. If you have any more responsive animation tips, pop them in the comment section. If there’s anything super helpful, I’ll add them to this compendium of information!

Addendum

One more note from Tom Miller as I was prepping this article:

I’m probably too late with this tip for your responsive animations article, but I highly recommend “finalize all the animations before building”. I’m currently retrofitting some site animations with “mobile versions”. Thank goodness for gsap.matchMedia… but I sure wish we’d known there’d be separate mobile layouts/animations from the beginning.

I think we all appreciate that this tip to “plan ahead” came at the absolute last minute. Thanks, Tom, and best of luck with those retrofits.


Responsive Animations for Every Screen Size and Device originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

CSS-Tricks

, , , , ,
[Top]

Scroll-Linked Animations With the Web Animations API (WAAPI) and ScrollTimeline

The Scroll-linked Animations specification is an upcoming and experimental addition that allows us to link animation-progress to scroll-progress: as you scroll up and down a scroll container, a linked animation also advances or rewinds accordingly.

We covered some use cases in a previous piece here on CSS-Tricks, all driven by the CSS @scroll-timeline at-rule and animation-timeline property the specification provides — yes, that’s correct: all those use cases were built using only HTML and CSS. No JavaScript.

Apart from the CSS interface we get with the Scroll-linked Animations specification, it also describes a JavaScript interface to implement scroll-linked animations. Let’s take a look at the ScrollTimeline class and how to use it with the Web Animations API.

Web Animations API: A quick recap

The Web Animations API (WAAPI) has been covered here on CSS-Tricks before. As a small recap, the API lets us construct animations and control their playback with JavaScript.

Take the following CSS animation, for example, where a bar sits at the top of the page, and:

  1. animates from red to darkred, then
  2. animates from zero width to full-width (by scaling the x-axis).

Translating the CSS animation to its WAAPI counterpart, the code becomes this:

new Animation(   new KeyframeEffect(     document.querySelector('.progressbar'),     {       backgroundColor: ['red', 'darkred'],       transform: ['scaleX(0)', 'scaleX(1)'],     },     {       duration: 2500,       fill: 'forwards',       easing: 'linear',     }   ) ).play();

Or alternatively, using a shorter syntax with Element.animate():

document.querySelector('.progressbar').animate(   {     backgroundColor: ['red', 'darkred'],     transform: ['scaleX(0)', 'scaleX(1)'],   },   {     duration: 2500,     fill: 'forwards',     easing: 'linear',    } );

In those last two JavaScript examples, we can distinguish two things. First, a keyframes object that describes which properties to animate:

{   backgroundColor: ['red', 'darkred'],   transform: ['scaleX(0)', 'scaleX(1)'], }

Second is an options Object that configures the animation duration, easing, etc.:

{   duration: 2500,   fill: 'forwards',   easing: 'linear', }

Creating and attaching a scroll timeline

To have our animation be driven by scroll — instead of the monotonic tick of a clock — we can keep our existing WAAPI code, but need to extend it by attaching a ScrollTimeline instance to it.

This ScrollTimeline class allows us to describe an AnimationTimeline whose time values are determined not by wall-clock time, but by the scrolling progress in a scroll container. It can be configured with a few options:

  • source: The scrollable element whose scrolling triggers the activation and drives the progress of the timeline. By default, this is document.scrollingElement (i.e. the scroll container that scrolls the entire document).
  • orientation: Determines the direction of scrolling, which triggers the activation and drives the progress of the timeline. By default, this is vertical (or block as a logical value).
  • scrollOffsets: These determine the effective scroll offsets, moving in the direction specified by the orientation value. They constitute equally-distanced in progress intervals in which the timeline is active.

These options get passed into the constructor. For example:

const myScrollTimeline = new ScrollTimeline({   source: document.scrollingElement,   orientation: 'block',   scrollOffsets: [     new CSSUnitValue(0, 'percent'),     new CSSUnitValue(100, 'percent'),   ], });

It’s not a coincidence that these options are exactly the same as the CSS @scroll-timeline descriptors. Both approaches let you achieve the same result with the only difference being the language you use to define them.

To attach our newly-created ScrollTimeline instance to an animation, we pass it as the second argument into the Animation constructor:

new Animation(   new KeyframeEffect(     document.querySelector('#progress'),     { transform: ['scaleX(0)', 'scaleX(1)'], },     { duration: 1, fill: 'forwards' }   ),   myScrollTimeline ).play();

When using the Element.animate() syntax, set it as the timeline option in the options object:

document.querySelector("#progress").animate(   {     transform: ["scaleX(0)", "scaleX(1)"]   },   {      duration: 1,      fill: "forwards",      timeline: myScrollTimeline   } );

With this code in place, the animation is driven by our ScrollTimeline instance instead of the default DocumentTimeline.

The current experimental implementation in Chromium uses scrollSource instead of source. That’s the reason you see both source and scrollSource in the code examples.

A word on browser compatibility

At the time of writing, only Chromium browsers support the ScrollTimeline class, behind a feature flag. Thankfully there’s the Scroll-Timeline Polyfill by Robert Flack that we can use to fill the unsupported gaps in all other browsers. In fact, all of the demos embedded in this article include it.

The polyfill is available as a module and registers itself if no support is detected. To include it, add the following import statement to your JavaScript code:

import 'https://flackr.github.io/scroll-timeline/dist/scroll-timeline.js';

The polyfill also registers the required CSS Typed Object Model classes, should the browser not support it. (👀 Looking at you, Safari.)

Advanced scroll timelines

Apart from absolute offsets, scroll-linked animations can also work with element-based offsets:

With this type of Scroll Offsets the animation is based on the location of an element within the scroll-container.

Typically this is used to animate an element as it comes into the scrollport until it has left the scrollport; e.g. while it is intersecting.

An element-based offset consists of three parts that describe it:

  1. target: The tracked DOM element.
  2. edge: This is what the ScrollTimeline’s source watches for the target to cross.
  3. threshold: A number ranging from 0.0 to 1.0 that indicates how much of the target is visible in the scroll port at the edge. (You might know this from IntersectionObserver.)

Here’s a visualization:

If you want to know more about element-based offsets, including how they work, and examples of commonly used offsets, check out this article.

Element-based offsets are also supported by the JS ScrollTimeline interface. To define one, use a regular object:

{   target: document.querySelector('#targetEl'),   edge: 'end',   threshold: 0.5, }

Typically, you pass two of these objects into the scrollOffsets property.

const $  image = document.querySelector('#myImage');  $  image.animate(   {     opacity: [0, 1],     clipPath: ['inset(45% 20% 45% 20%)', 'inset(0% 0% 0% 0%)'],   },   {     duration: 1,     fill: "both",     timeline: new ScrollTimeline({       scrollSource: document.scrollingElement,       timeRange: 1,       fill: "both",       scrollOffsets: [         { target: $  image, edge: 'end', threshold: 0.5 },         { target: $  image, edge: 'end', threshold: 1 },       ],     }),   } );

This code is used in the following demo below. It’s a JavaScript-remake of the effect I covered last time: as an image scrolls into the viewport, it fades-in and becomes unmasked.

More examples

Here are a few more examples I cooked up.

Horizontal scroll section

This is based on a demo by Cameron Knight, which features a horizontal scroll section. It behaves similarly, but uses ScrollTimeline instead of GSAP’s ScrollTrigger.

For more on how this code works and to see a pure CSS version, please refer to this write-up.

CoverFlow

Remember CoverFlow from iTunes? Well, here’s a version built with ScrollTimeline:

This demo does not behave 100% as expected in Chromium due to a bug. The problem is that the start and end positions are incorrectly calculated. You can find an explanation (with videos) in this Twitter thread.

More information on this demo can be found in this article.

CSS or JavaScript?

There’s no real difference using either CSS or JavaScript for the Scroll-linked Animations, except for the language used: both use the same concepts and constructs. In the true spirit of progressive enhancement, I would grab to CSS for these kind of effects.

However, as we covered earlier, support for the CSS-based implementation is fairly poor at the time of writing:

Because of that poor support, you’ll certainly get further with JavaScript at this very moment. Just make sure your site can also be viewed and consumed when JavaScript is disabled. 😉


The post Scroll-Linked Animations With the Web Animations API (WAAPI) and ScrollTimeline appeared first on CSS-Tricks. You can support CSS-Tricks by being an MVP Supporter.

CSS-Tricks

, , ,
[Top]

Fire SVG animations (SMIL) when the SVG is visible

When requirements read “when visible” your brain should go straight to IntersectionObserver. That’s exactly what Zach is doing here to kick off an animation when it scrolls into view.

Except this animation is an SVG SMIL animation: an <animate> situation. SMIL animations have some kinda cool things they can do, like begin when another animation ends, which is something CSS doesn’t help with that much. Turns out SMIL has a JavaScript API as well, so it’s possible to kick off the animation on demand that way, while also respecting prefers-reduced-motion.

Also check this out:

.querySelectorAll(`:scope [begin="indefinite"]`);

That :scope thing is new to me.

Direct Link to ArticlePermalink


The post Fire SVG animations (SMIL) when the SVG is visible appeared first on CSS-Tricks. You can support CSS-Tricks by being an MVP Supporter.

CSS-Tricks

, , ,
[Top]

Practical Use Cases for Scroll-Linked Animations in CSS with Scroll Timelines

The Scroll-Linked Animations specification is an upcoming and experimental addition to CSS. Using the @scroll-timeline at-rule and animation-timeline property this specification provides you can control the time position of regular CSS Animations by scrolling.

In this post, we take a look at some practical use cases where scroll-linked animations come in handy, replacing a typical JavaScript approach.

👨‍🔬 The CSS features described in this post are still experimental and not finalized at all. The are not supported by any browser at the time of writing, except for Chromium ≥ 89 with the #experimental-web-platform-features flag enabled.

CSS Scroll-Linked Animations, a quick primer

With CSS Scroll-Linked Animations, you can drive a CSS animation by scroll: as you scroll up or down inside a scroll container, the linked CSS animation will advance or rewind. Best of all is that this is all running off main thread, on the compositor.

You need three things to implement a basic scroll-linked animation:

  1. a CSS animation
  2. a scroll timeline
  3. a link between both

CSS animation

This is a regular CSS Animation like we already know:

@keyframes adjust-progressbar {   from {     transform: scaleX(0);   }   to {     transform: scaleX(1);   } }

As you normally do, attach it to an element using the animation property:

#progressbar {   animation: 1s linear forwards adjust-progressbar; }

Scroll timeline

The scroll timeline allows us to map the scroll distance to the animation progress. In CSS, we describe this with the CSS @scroll-timeline at-rule.

@scroll-timeline scroll-in-document-timeline {   source: auto;   orientation: vertical;   scroll-offsets: 0%, 100%; }

This at-rule consists of descriptors, including:

  1. The source describes the scrollable element whose scrolling triggers the activation and drives the progress of the timeline. By default, this is the entire document.
  2. The orientation determines the scrolling direction that should trigger the animation. By default, this is vertical.
  3. The scroll-offsets property is an array of key points that describe the range in which the animation should be active. It can be absolute values (e.g. percentages and lengths) or element-based.

A previous version of the specification required you to also set a time-range descriptor. This descriptor has been removed and will automatically take over the animation-duration from the linked animation. You may still see traces of it in the demos, but you can safely ignore it.

To associate our @scroll-timeline with our CSS animation, we use the new animation-timeline CSS property, and have it refer to the timeline’s name.

#progressbar {   animation: 1s linear forwards adjust-progressbar;   animation-timeline: scroll-in-document-timeline; /* 👈 THIS! */ }

With that set up the adjust-progressbar animation won’t run automatically on page load, but will only advance as we scroll down the page.

For a more in-depth introduction to @scroll-timeline please refer to Part 1 and Part 2 of my series on the future of scroll-linked animations.

The first post looks at each descriptor/property in more detail, explaining them with an example to go along with them, before covering many more interesting demos.

The second post digs even deeper, looking into Element-Based Offsets, which allow us to drive an animation as an element appears into and disappears from the scrollport as we scroll.

An example of what you can achieve with CSS Scroll-Linked Animations using Element-Based Offsets.

Practical use cases

Apart from the progress par demo above, there are a few more use cases or scenarios where scroll-linked animations can replace a solution typically implemented using JavaScript.

  1. parallax header
  2. image reveal/hide
  3. typing animation
  4. carousel indicators
  5. scrollspy

Parallax header

A typical use case for Scroll-Linked Animations is a parallax effect, where several sections of a page seem to have a different scrolling speed. There’s a way to create these type of effects using only CSS, but that requires mind-bending transform hacks involving translate-z() and scale().

Inspired upon the Firewatch Header—which uses the mentioned transform hack—I created this version that uses a CSS scroll timeline:

Compared to the original demo:

  • The markup was kept, except for that extra .parallax__cover that’s no longer needed.
  • The <body> was given a min-height to create some scroll-estate.
  • The positioning of the .parallax element and its .parallax_layer child elements was tweaked.
  • The transform/perspective-hack was replaced with a scroll timeline.

Each different layer uses the same scroll timeline: scroll over a distance of 100vh.

@scroll-timeline scroll-for-100vh {   time-range: 1s;   scroll-offsets: 0, 100vh; }  .parallax__layer {   animation: 1s parallax linear;   animation-timeline: scroll-for-100vh; }

What’s different between layers is the distance that they move as we scroll down:

  • The foremost layer should stay in place, eg. move for 0vh.
  • The last layer should should move the fastest, e.g. 100vh.
  • All layers in between are interpolated.
@keyframes parallax {   to {     transform: translateY(var(--offset));   } }  .parallax__layer__0 {   --offset: 100vh; }  .parallax__layer__1 {   --offset: 83vh; }  .parallax__layer__2 {   --offset: 67vh; }  .parallax__layer__3 {   --offset: 50vh; }  .parallax__layer__4 {   --offset: 34vh; }  .parallax__layer__5 {   --offset: 17vh; }  .parallax__layer__6 {   --offset: 0vh; }

As the foremost layers move over a greater distance, they appear to move faster than the lower layers, achieving the parallax effect.

Image reveal/hide

Another great use-case for scroll linked animations is an image reveal: an image slides into view as it appears.

By default, the image is given an opacity of 0 and is masked using a clip-path:

#revealing-image {   opacity: 0;   clip-path: inset(45% 20% 45% 20%); }

In the end-state we want the image to be fully visible, so we sent the end-frame of our animation to reflect that:

@keyframes reveal {   to {     clip-path: inset(0% 0% 0% 0%);     opacity: 1;   } }

By using element-based offsets as our scroll timeline offsets, we can have it so thar the image begins to appear only when the image itself slides into view.

@scroll-timeline revealing-image-timeline {   scroll-offsets:     selector(#revealing-image) end 0.5,     selector(#revealing-image) end 1   ; }  #revealing-image {   animation: reveal 1s linear forwards;   animation-timeline: revealing-image-timeline; }

😵 Can’t follow with those element-based offsets? This visualization/tool has got you covered.

Typing animation

As CSS scroll timelines can be linked to any existing CSS animation, you can basically take any CSS Animation demo and transform it. Take this typing animation for example:

With the addition of a scroll timeline and the animation-timeline property, it can be adjusted to “type on scroll”:

Note that to create some scroll-estate the <body>was also given a height of 300vh.

Using a different animation, the code above can easily be adjusted to create a zoom on scroll effect:

I can see these two working great for article intros.

One of the components of a carousel (aka slider) is an indicator that exposes how many slides it contains, as well as which slide is currently active. This is typically done using bullets.

This again is something we will be able to achieve using a CSS scroll timeline, as shown in this demo created by Fabrizio Calderan:

The active state bullet is injected via .slider nav::before and has an animation set that moves it over the other bullets

/* Styling of the dots */ .slider nav::before, .slider a {   inline-size: 1rem;   aspect-ratio: 1;   border-radius: 50%;   background: #9bc; }  /* Positioning of the active dot */ .slider nav::before {   content: "";   position: absolute;   z-index: 1;   display: block;   cursor: not-allowed;   transform: translateX(0);   animation: dot 1s steps(1, end) 0s forwards; }  /* Position over time of the active dot */ @keyframes dot {   0%      { transform: translateX(0); }   33%      { transform: translateX(calc((100% + var(--gap)) * 1)); }   66%      { transform: translateX(calc((100% + var(--gap)) * 2)); }    100%      { transform: translateX(calc((100% + var(--gap)) * 3)); } }

By attaching a @scroll-timeline onto the slider, the dot that indicates the active state can move as you scroll:

@scroll-timeline slide {   source: selector(#s);   orientation: inline;  }  .slider nav::before {   /* etc. */   animation-timeline: slide; }

The dot only moves after the slide has snapped to its position thanks to the inclusion of a steps() function in the animation. When removing it, it becomes more clear how the dot moves as you scroll

💡 This feels like the final missing piece to Christian Shaefer’s CSS-only carousel.

ScrollSpy

Back in early 2020, I created a sticky table of contents with scrolling active states. The final part to creating the demo was to use IntersectionObserver to set the active states in the table of contents (ToC) as you scroll up/down the document.

Unlike the carousel indicators demo from above we can’t simply get there by moving a single dot around, as it’s the texts in the ToC that get adjusted. To approach this situation, we need to attach two animations onto each element in the ToC:

  1. The first animation is to visually activate the ToC item when the proper section comes into view at the bottom edge of the document.
  2. The second animation is to visually deactivate the ToC item when the proper section slides out of view at the top edge of the document.
.section-nav li > a {   animation:     1s activate-on-enter linear forwards,     1s deactivate-on-leave linear forwards; }

As we have two animations, we also need to create two scroll timelines, and this for each section of the content. Take the #introduction section for example:

@scroll-timeline section-introduction-enter {   source: auto;   scroll-offsets:     selector(#introduction) end 0,     selector(#introduction) end 1; }  @scroll-timeline section-introduction-leave {   source: auto;   scroll-offsets:     selector(#introduction) start 1,     selector(#introduction) start 0; }

Once both of these timelines are linked to both animations, everything will work as expected:

.section-nav li > a[href"#introduction"] {   animation-timeline:     section-introduction-enter,     section-introduction-leave; }

In closing

I hope I have convinced you of the potential offered by the CSS Scroll-linked Animations specification. Unfortunately, it’s only supported in Chromium-based browsers right now, hidden behind a flag.

Given this potential, I personally hope that—once the specification settles onto a certain syntax—other browser vendors will follow suit. If you too would like to see Scroll-Linked Animations land in other browsers, you can actively star/follow the relevant browser issues.

By actively starring issues, us developers can signal our interest into these features to browser vendors.


The post Practical Use Cases for Scroll-Linked Animations in CSS with Scroll Timelines appeared first on CSS-Tricks. You can support CSS-Tricks by being an MVP Supporter.

CSS-Tricks

, , , , ,
[Top]

Coordinating Svelte Animations With XState

This post is an introduction to XState as it might be used in a Svelte project. XState is unique in the JavaScript ecosystem. It doesn’t keep your DOM synced with your application state, nor does it help you with asynchrony, or streams of data; XState helps manage your application’s state by allowing you to model your state as a finite state machine (FSM).

A deep dive into state machines and formal languages is beyond the scope of this post, but Jon Bellah does that in another CSS-Tricks article. For now, think of an FSM as a flow chart. Flow charts have a number of states, represented as bubbles, and arrows leading from one state to the next, signifying a transition from one state to the next. State machines can have more than one arrow leading out of a state, or none at all if it’s a final state, and they can even have arrows leaving a state, and pointing right back into that same state.

If that all sounds overwhelming, relax, we’ll get into all the details, nice and slow. For now, the high level view is that, when we model our application as a state machine, we’ll be creating different “states” our application can be in (get it … state machine … states?), and the events that happen and cause changes to state will be the arrows between those states. XState calls the states “states,” and the arrows between the states “actions.”

Our example

XState has a learning curve, which makes it challenging to teach. With too contrived a use case it’ll appear needlessly complex. It’s only when an application’s code gets a bit tangled that XState shines. This makes writing about it tricky. With that said, the example we’ll look at is an autocomplete widget (sometimes called autosuggest), or an input box that, when clicked, reveals a list of items to choose from, which filter as you type in the input.

For this post we’ll look at getting the animation code cleaned up. Here’s the starting point:

This is actual code from my svelte-helpers library, though with unnecessary pieces removed for this post. You can click the input and filter the items, but you won’t be able to select anything, “arrow down” through the items, hover, etc. I’ve removed all the code that’s irrelevant to this post.

We’ll be looking at the animation of the list of items. When you click the input, and the results list first renders, we want to animate it down. As you type and filter, changes to the list’s dimensions will animate larger and smaller. And when the input loses focus, or you click ESC, we animate the list’s height to zero, while fading it out, and then remove it from the DOM (and not before). To make things more interesting (and nice for the user), let’s use a different spring configuration for the opening than what we use for the closing, so the list closes a bit more quickly, or stiffly, so unneeded UX doesn’t linger on the screen too long.

If you’re wondering why I’m not using Svelte transitions to manage the animations in and out of the DOM, it’s because I’m also animating the list’s dimensions when it’s open, as the user filters, and coordinating between transition, and regular spring animations is a lot harder than simply waiting for a spring update to finish getting to zero before removing an element from the DOM. For example, what happens if the user quickly types and filters the list, as it’s animating in? As we’ll see, XState makes tricky state transitions like this easy.

Scoping the Problem

Let’s take a look at the code from the example so far. We’ve got an open variable to control when the list is open, and a resultsListVisible property to control whether it should be in the DOM. We also have a closing variable that controls whether the list is in the process of closing.

On line 28, there’s an inputEngaged method that runs when the input is clicked or focused. For now let’s just note that it sets open and resultsListVisible to true. inputChanged is called when the user types in the input, and sets open to true. This is for when the input is focused, the user clicks escape to close it, but then starts typing, so it can re-open. And, of course, the inputBlurred function runs when you’d expect, and sets closing to true, and open to false.

Let’s pick apart this tangled mess and see how the animations work. Note the slideInSpring and opacitySpring at the top. The former slides the list up and down, and adjusts the size as the user types. The latter fades the list out when hidden. We’ll focus mostly on the slideInSpring.

Take a look at the monstrosity of a function called setSpringDimensions. This updates our slide spring. Focusing on the important pieces, we take a few boolean properties. If the list is opening, we set the opening spring config, we immediately set the list’s width (I want the list to only slide down, not down and out), via the { hard: true } config, and then set the height. If we’re closing, we animate to zero, and, when the animation is complete, we set resultsListVisible to false (if the closing animation is interrupted, Svelte will be smart enough to not resolve the promise so the callback will never run). Lastly, this method is also called any time the size of the results list changes, i.e., as the user filters. We set up a ResizeObserver elsewhere to manage this.

Spaghetti galore

Let’s take stock of this code.

  • We have our open variable which tracks if the list is open.
  • We have the resultsListVisible variable which tracks if the list should be in the DOM (and set to false after the close animation is complete).
  • We have the closing variable that tracks if the list is in the process of closing, which we check for in the input focus/click handler so we can reverse the closing animation if the user quickly re-engages the widget before it’s done closing.
  • We also have setSpringDimensions that we call in four different places. It sets our springs depending on whether the list is opening, closing, or just resizing while open (i.e. if the user filters the list).
  • Lastly, we have a resultsListRendered Svelte action that runs when the results list DOM element renders. It starts up our ResizeObserver, and when the DOM node unmounts, sets closing to false.

Did you catch the bug? When the ESC button is pressed, I’m only setting open to false. I forgot to set closing to true, and call setSpringDimensions(false, true). This bug was not purposefully contrived for this blog post! That’s an actual mistake I made when I was overhauling this widget’s animations. I could just copy paste the code in inputBlured over to where the escape button is caught, or even move it to a new function and call it from both places. This bug isn’t fundamentally hard to solve, but it does increase the cognitive load of the code.

There’s a lot of things we’re keeping track of, but worst of all, this state is scattered all throughout the module. Take any piece of state described above, and use CodeSandbox’s Find feature to view all the places where that piece of state is used. You’ll see your cursor bouncing across the file. Now imagine you’re new to this code, trying to make sense of it. Think about the growing mental model of all these state pieces that you’ll have to keep track of, figuring out how it works based on all the places it exists. We’ve all been there; it sucks. XState offers a better way; let’s see how.

Introducing XState

Let’s step back a bit. Wouldn’t it be simpler to model our widget in terms of what state it’s in, with events happening as the user interacts, which cause side effects, and transitions to new states? Of course, but that’s what we were already doing; the problem is, the code is scattered everywhere. XState gives us the ability to properly model our state in this way.

Setting expectations

Don’t expect XState to magically make all of our complexity vanish. We still need to coordinate our springs, adjust the spring’s config based on opening and closing states, handle resizes, etc. What XState gives us is the ability to centralize this state management code in a way that’s easy to reason about, and adjust. In fact, our overall line count will increase a bit, as a result of our state machine setup. Let’s take a look.

Your first state machine

Let’s jump right in, and see what a bare bones state machine looks like. I’m using XState’s FSM package, which is a minimal, pared down version of XState, with a tiny 1KB bundle size, perfect for libraries (like an autosuggest widget). It doesn’t have a lot of advanced features like the full XState package, but we wouldn’t need them for our use case, and we wouldn’t want them for an introductory post like this.

The code for our state machine is below, and the interactive demo is over at Code Sandbox. There’s a lot, but we’ll go over it shortly. And to be clear, it doesn’t work yet.

const stateMachine = createMachine(   {     initial: "initial",     context: {       open: false,       node: null     },     states: {       initial: {         on: { OPEN: "open" }       },       open: {         on: {           RENDERED: { actions: "rendered" },           RESIZE: { actions: "resize" },           CLOSE: "closing"         },         entry: "opened"       },       closing: {         on: {           OPEN: { target: "open", actions: ["resize"] },           CLOSED: "closed"         },         entry: "close"       },       closed: {         on: {           OPEN: "open"         },         entry: "closed"       }     }   },   {     actions: {       opened: assign(context => {         return { ...context, open: true };       }),       rendered: assign((context, evt) => {         const { node } = evt;         return { ...context, node };       }),       close() {},       resize(context) {},       closed: assign(() => {         return { open: false, node: null };       })     }   } );

Let’s go from top to bottom. The initial property controls what the initial state is, which I’ve called “initial.” context is the data associated with our state machine. I’m storing a boolean for whether the results list is currently open, as well as a node object for that same results list. Next we see our states. Each state is a key in the states property. For most states, you can see we have an on property, and an entry property.

on configures events. For each event, we can transition to a new state; we can run side effects, called actions; or both. For example, when the OPEN event happens inside of the initial state, we move into the open state. When the RENDERED event happens in the open state, we run the rendered action. And when the OPEN event happens inside the closing state, we transition into the open state, and also run the resize action. The entry field you see on most states configures an action to run automatically whenever a state is entered. There are also exit actions, although we don’t need them here.

We still have a few more things to cover. Let’s look at how our state machine’s data, or context, can change. When we want an action to modify context, we wrap it in assign and return the new context from our action; if we don’t need any processing, we can just pass the new state directly to assign. If our action does not update context, i.e., it’s just for side effects, then we don’t wrap our action function in assign, and just perform whatever side effects we need.

Affecting change in our state machine

We have a cool model for our state machine, but how do we run it? We use the interpret function.

const stateMachineService = interpret(stateMachine).start();

Now stateMachineService is our running state machine, on which we can invoke events to force our transitions and actions. To fire an event, we call send, passing the event name, and then, optionally, the event object. For example, in our Svelte action that runs when the results list first mounts in the DOM, we have this:

stateMachineService.send({ type: "RENDERED", node });

That’s how the rendered action gets the node for the results list. If you look around the rest of the AutoComplete.svelte file, you’ll see all the ad hoc state management code replaced with single line event dispatches. In the event handler for our input click/focus, we run the OPEN event. Our ResizeObserver fires the RESIZE event. And so on.

Let’s pause for a moment and appreciate the things XState gives us for free here. Let’s look at the handler that runs when our input is clicked or focused before we added XState.

function inputEngaged(evt) {   if (closing) {     setSpringDimensions();   }   open = true;   resultsListVisible = true; } 

Before, we were checking to see if we were closing, and if so, forcing a re-calculation of our sliding spring. Otherwise we opened our widget. But what happened if we clicked on the input when it was already open? The same code re-ran. Fortunately that didn’t really matter. Svelte doesn’t care if we re-set open and resultsListVisible to the values they already held. But those concerns disappear with XState. The new version looks like this:

 function inputEngaged(evt) {   stateMachineService.send("OPEN"); }

If our state machine is already in the open state, and we fire the OPEN event, then nothing happens, since there’s no OPEN event configured for that state. And that special handling for when the input is clicked when the results are closing? That’s also handled right in the state machine config — notice how the OPEN event tacks on the resize action when it’s run from the closing state.

And, of course, we’ve fixed the ESC key bug from before. Now, pressing the key simply fires the CLOSE event, and that’s that.

Finishing up

The ending is almost anti-climactic. We need to take all of the work we were doing before, and simply move it to the right place among our actions. XState does not remove the need for us to write code; it only provides a structured, clear place to put it.

{   actions: {     opened: assign({ open: true }),     rendered: assign((context, evt) => {       const { node } = evt;       const dimensions = getResultsListDimensions(node);       itemsHeightObserver.observe(node);       opacitySpring.set(1, { hard: true });       Object.assign(slideInSpring, SLIDE_OPEN);       slideInSpring.update(prev => ({ ...prev, width: dimensions.width }), {         hard: true       });       slideInSpring.set(dimensions, { hard: false });       return { ...context, node };     }),     close() {       opacitySpring.set(0);       Object.assign(slideInSpring, SLIDE_CLOSE);       slideInSpring         .update(prev => ({ ...prev, height: 0 }))         .then(() => {           stateMachineService.send("CLOSED");         });     },     resize(context) {       opacitySpring.set(1);       slideInSpring.set(getResultsListDimensions(context.node));     },     closed: assign(() => {       itemsHeightObserver.unobserve(resultsList);       return { open: false, node: null };     })   } }

Odds and ends

Our animation state is in our state machine, but how do we get it out? We need the open state to control our results list rendering, and, while not used in this demo, the real version of this autosuggest widget needs the results list DOM node for things like scrolling the currently highlighted item into view.

It turns out our stateMachineService has a subscribe method that fires whenever there’s a state change. The callback you pass is invoked with the current state machine state, which includes a context object. But Svelte has a special trick up its sleeve: its reactive syntax of $ : doesn’t only work with component variables and Svelte stores; it also works with any object with a subscribe method. That means we can sync with our state machine with something as simple as this:

$  : ({ open, node: resultsList } = $  stateMachineService.context);

Just a regular destructuring, with some parens to help things get parsed correctly.

One quick note here, as an area for improvement. Right now, we have some actions which both both perform a side effect, and also update state. Ideally, we should probably split these up into two actions, one just for the side effect, and the other using assign for the new state. But I decided to keep things as simple as possible for this article to help ease the introduction of XState, even if a few things wound up not being quite ideal.

Parting thoughts

I hope this post has sparked some interest in XState. I’ve found it to be an incredibly useful, easy to use tool for managing complex state. Please know that we’ve only scratched the surface. We focused on the minimal fsm package, but the entire XState library is capable of a lot more than what we covered here, from nested states, to first-class support for Promises, and it even has a state visualization tool! I urge you to check it out.

Happy coding!


The post Coordinating Svelte Animations With XState appeared first on CSS-Tricks.

You can support CSS-Tricks by being an MVP Supporter.

CSS-Tricks

, , ,
[Top]

Platform News: Rounded Outlines, GPU-Accelerated SVG Animations, How CSS Variables Are Resolved

In the news this week, Firefox gets rounded outlines, SVG animations are now GPU-accelerated in Chrome, there are no physical units in CSS, The New York Times crossword is accessible, and CSS variables are resolved before the value is inherited.

Let’s jump in the news!

Rounded outlines are coming to Firefox

The idea to have the outline follow the border curve has existed ever since it became possible to create rounded borders via the border-radius property in the mid 2000s. It was suggested to Mozilla, WebKit, and Chromium over ten years ago, and it’s even been part of the CSS UI specification since 2015:

The parts of the outline are not required to be rectangular. To the extent that the outline follows the border edge, it should follow the border-radius curve.

Fast-forward to today in 2021 and outlines are still rectangles in every browser without exception:

But this is finally starting to change. In a few weeks, Firefox will become the first browser with rounded outlines that automatically follow the border shape. This will also apply to Firefox’s default focus outline on buttons.

Three sets of round yellow buttons, comparing how Chrome, Firefox, and Safari handle outlines.

Please star Chromium Issue #81556 (sign in required) to help prioritize this bug and bring rounded outlines to Chrome sooner rather than later.

SVG animations are now GPU-accelerated in Chrome

Until recently, animating an SVG element via CSS would trigger repaint on every frame (usually 60 times per second) in Chromium-based browsers. Such constant repainting can have a negative impact on the smoothness of the animation and the performance of the page itself.

The latest version of Chrome has eliminated this performance issue by enabling hardware acceleration for SVG animations. This means that SVG animations are offloaded to the GPU and no longer run on the main thread.

Side by side comparison of the Performance tab in Chrome DevTools.
In this example, the SVG circle is continuously faded in and out via a CSS animation (see code)

The switch to GPU acceleration automatically made SVG animations more performant in Chromium-based browsers (Firefox does this too), which is definitely good news for the web:

Hooray for more screen reader-accessible, progressively enhanced SVG animations and less Canvas.

There cannot be real physical units in CSS

CSS defines six physical units, including in (inches) and cm (centimeters). Every physical unit is in a fixed ratio with the pixel unit, which is the canonical unit. For example, 1in is always exactly 96px. On most modern screens, this length does not correspond to 1 real-world inch.

The FAQ page of the CSS Working Group now answers the question why there can’t be real physical units in CSS. In short, the browser cannot always determine the exact size and resolution of the display (think projectors). For websites that need accurate real-world units, the Working Group recommends per-device calibration:

Have a calibration page, where you ask the user to measure the distance between two lines that are some CSS distance apart (say, 10cm), and input the value they get. Use this to find the scaling factor necessary for that screen (CSS length divided by user-provided length).

This scaling factor can then be set to a custom property and used to compute accurate lengths in CSS:

html {   --unit-scale: 1.428; }  .box {   /* 5 real-world centimeters */   width: calc(5cm * var(--unit-scale, 1)); }

The Times crossword is accessible to screen reader users

The NYT Open team wrote about some of the improvements to the New York Times website that have made it more accessible in recent years. The website uses semantic HTML (<article>, <nav>, etc.), increased contrast on important components (e.g., login and registration), and skip-to-content links that adapt to the site’s paywall.

Furthermore, the Games team made the daily crossword puzzle accessible to keyboard and screen reader users. The crossword is implemented as a grid of SVG <rect> elements. As the user navigates through the puzzle, the current square’s aria-label attribute (accessible name) is dynamically updated to provide additional context.

Screenshot of the crossword game with an open screen reader dialog announcing what is on the screen.
The screen reader announces the clue, the number of letters in the solution, and the position of the selected square

You can play the mini crossword without an account. Try solving the puzzle with the keyboard.

CSS variables are resolved before the value is inherited

Yuan Chuan recently shared a little CSS quiz that I didn’t answer correctly because I wasn’t sure if a CSS variable (the var() function) is resolved before or after the value is inherited. I’ll try to explain how this works on the following example:

html {   --text-color: var(--main-color, black); }  footer {   --main-color: brown; }  p {   color: var(--text-color); }

The question: Is the color of the paragraph in the footer black or brown? There are two possibilities. Either (A) the declared values of both custom properties are inherited to the paragraph, and then the color property resolves to brown, or (B) the --text-color property resolves to black directly on the <html> element, and then this value is inherited to the paragraph and assigned to the color property.

Two CSS rulesets, one as Option A and the other as Option B, both showing how variables are inherited and resolved between elements.

The correct answer is option B (the color is black). CSS variables are resolved before the value is inherited. In this case, --text-color falls back to black because --main-color does not exist on the <html> element. This rule is specified in the CSS Variables module:

It is important to note that custom properties resolve any var() functions in their values at computed-value time, which occurs before the value is inherited.


The post Platform News: Rounded Outlines, GPU-Accelerated SVG Animations, How CSS Variables Are Resolved appeared first on CSS-Tricks.

You can support CSS-Tricks by being an MVP Supporter.

CSS-Tricks

, , , , , , ,
[Top]

How to Play and Pause CSS Animations with CSS Custom Properties

Let’s have a look CSS @keyframes animations, and specifically about how you can pause and otherwise control them. There is a CSS property specifically for it, that can be controlled with JavaScript, but there is plenty of nuance to get into in the details. We’ll also look at my preferred way of setting this up which gives lots of control. Hint: it involves CSS custom properties.

The importance of pausing animations

Recently, while working on the CSS-powered slideshow you’ll see later in this article, I was inspecting the animations in the Layers panel of DevTools. I noticed something interesting I’d never thought about before: animations not currently in the viewport were still running!

Maybe it’s not that unexpected. We know videos do that. Videos just go on until you pause them. But it made me wonder if these playing animations still use the CPU/GPU? Do they consume unnecessary processing power, slowing down other parts of the page?

Inspecting frames in the Performance panel in DevTools didn’t shed any more light on this since I couldn’t see “offscreen”-frames. But, when I scrolled away from my “CSS Only Slideshow” at the first slide, then waited and scrolled back, it was at slide five. The animation hadn’t paused. Animations just run and run, until you pause them.

So I began to look into how, why and when animations should pause. Performance is an obvious reason, given the findings above. Another reason is control. Users not only love to have control, but they should have control. A couple of years ago, my wife had a really bad concussion. Since then, she has avoided webpages with too many animations, as they make her dizzy. As a result, I consider accessibility perhaps the most important reason for allowing animations to pause.

All together, this is important stuff. We’re talking specifically about CSS keyframe animations, but broadly, that means we’re talking about:

  1. Performance
  2. Control
  3. Accessibility

The basics of pausing an animation

The only way to truly pause an animation in CSS is to use the animation-play-state property with a paused value.

.paused {   animation-play-state: paused; }

In JavaScript, the property is “camelCased” as animationPlayState and set like this:

element.style.animationPlayState = 'paused';

We can create a toggle that plays and pauses the animation by reading the current value of animationPlayState:

const running = element.style.animationPlayState === 'running';

…and then setting it to the opposite value:

element.style.animationPlayState = running ? 'paused' : 'running';

Setting the duration

Another way to pause animations is to set animation-duration to 0s. The animation is actually running, but since it has no duration, you won’t see any action.

But if we change the value to 3s instead:

It works, but has a major caveat: the animations are technically still running. The animation is merely toggling between its initial position, and where it is next in the sequence.

Straight up removing the animation

We can remove the animation entirely and add it back via classes, but like animation-duration, this doesn’t actually pause the animation.

.remove-animation {   animation: none !important; }

Since true pausing is really what we’re after here, let’s stick with animation-play-state and look into other ways of using it.

Using data attributes and CSS custom properties

Let’s use a data-attribute as a selector in our CSS. We can call those whatever we want, so I’m going to use a [data-animation]-attribute on all the elements where I’d like to play/pause animations. That way, it can be distinguished from other animations:

<div data-animation></div>

That attribute is the selector, and the animation shorthand is the property where we’re setting everything. We’ll toss in a bunch of CSS custom properties *(*using Emmet-abbreviations) as values:

[data-animation] {   animation:     var(--animn, none)     var(--animdur, 1s)     var(--animtf, linear)     var(--animdel, 0s)     var(--animic, infinite)     var(--animdir, alternate)     var(--animfm, none)     var(--animps, running); }

With that in place, any animation with this data-attribute will be perfectly ready to accept animations, and we can control individual aspects of the animation with custom properties. Some animations are going to have something in common (like duration, easing-type, etc.), so fallback values are set on the custom properties as well.

Why CSS custom properties? First of all, they can be read and set in both CSS and JavaScript. Secondly, they help significantly reduce the amount of CSS we need to write. And, since we can set them within @keyframes (at least in Chrome at the time of writing), they offer new and exiting ways to work with animations!

For the animations themselves, I’m using class selectors and updating the variables from the [data-animation]-selector:

<div class="circle a-slide" data-animation></div>

Why a class and a data-attribute? At this stage, the data-animation attribute might as well be a regular class, but we’re going to use it in more advanced ways later. Note that the .circle class name actually has nothing to do with the animation — it’s just a class for styling the element.

/* Animation classes */ .a-pulse {   --animn: pulse; } .a-slide {   --animdur: 3s;   --animn: slide; }  /* Keyframes */ @keyframes pulse {   0% { transform: scale(1); }   25% { transform: scale(.9); }   50% { transform: scale(1); }   75% { transform: scale(1.1); }   100% { transform: scale(1); } } @keyframes slide {   from { margin-left: 0%; }   to { margin-left: 150px; } }

We only need to update the values that will change, so if we use some common values in the fallback values for the data-animation selector, we only need to update the name of the animation’s custom property, --animn.

Example: Pausing with the checkbox hack

To pause all the animations using the ol’ checkbox hack, let’s create a checkbox before the animations:

<input type="checkbox" data-animation-pause />

And update the --animps property when checked:

[data-animation-pause]:checked ~ [data-animation] {   --animps: paused; }

That’s it! The animations toggle between played and paused when clicking the checkbox — no JavaScript required.

CSS-only slideshow

Let’s put some of these ideas to work!

I‘ve played with the <details>-tag a lot recently. It’s the obvious candidate for accordions, but it can also be used for tooltips, toggle-tips, drop-downs (styled <select>-look-a-likes), mega-menus… you name it. It is the official HTML disclosure element, after all. Apart from the global attributes and global events that all HTML elements accept, <details> has a single open attribute, and a single toggle event. So, like the checkbox hack, it’s perfect for toggling state — but even simpler:

details[open] {   --state: 1; } details:not([open]) {   --state: 0; }

I decided to do a slideshow, where the slides change automatically via a primary animation called autoplay, and each individual slide has its own unique secondary animation. The animation-play-state is controlled by the --animps-property. Each individual slide can have it’s own, unique animation, defined in a --animn-property:

<figure style="--animn:kenburns-top;--index:0;">   <img src="some-slide-image.jpg" />   <figcaption>Caption</figcaption> </figure>

The animation-play-state of the secondary animations are controlled by the --img-animps-property. I found a bunch of nice Ken Burns-esque animations at Animista and switched between them in the --animn-properties of the slides.

Pausing an animation from another animation

In order to prevent GPU overload, it would be ideal for the primary animation to pause any secondary animations. We noted it briefly earlier, but only Chrome (at the time of writing, and it is a bit shaky) can update a CSS Custom Property from an @keyframe animation — which you can see in the following example where the --bgc-property and --counter-properties are modified at different frames:

The initial state of the secondary animation, the --img-animps -property, needs to be paused, even if the primary animation is running:

details[open] ~ .c-mm__inner .c-mm__frame {   --animps: running;   --img-animps: paused; }

Then, in the main animation @keyframes, the property is updated to running:

@keyframes autoplay {   0.1% {     --img-animps: running; /* START */     opacity: 0;     z-index: calc(var(--z) + var(--slides))   }   5% { opacity: 1 }   50% { opacity: 1 }   51% { --img-animps: paused } /* STOP! */   100% {     opacity: 0;     z-index: var(--z)   } }

To make this work in browsers other than Chrome, the initial value needs to be running, as they cannot update a CSS custom property from a @keyframe.

Here’s the slideshow, with a “details hack” play/pause-button — no JavaScript required:

Enabling prefers-reduced-motion

Some people prefer no animations, or at least reduced motion. It might just be a personal preference, but can also be because of a medical condition. We talked about the importance of accessibility with animations at the very top of this post.

Both macOS and Windows have options that allow users to inform browsers that they prefer reduced motion on websites. This enables us to reach for the prefers-reduced-motion feature query, which Eric Bailey has written all about.

@media (prefers-reduced-motion) { ... }

Let’s use the [data-animation]-selector for reduced motion by giving it different values that are applied when prefers-reduced-motion is enabled*:*

  • alternate = run a different animation
  • once = set the animation-iteration-count to 1
  • slow = change the animation-duration-property
  • stop = set animation-play-state to paused

These are just suggestions and they can be anything you want, really.

<div class="circle a-slide" data-animation="alternate"></div> <div class="circle a-slide" data-animation="once"></div> <div class="circle a-slide" data-animation="slow"></div> <div class="circle a-slide" data-animation="stop"></div>

And the updated media query:

@media (prefers-reduced-motion) {   [data-animation="alternate"] {    /* Change animation duration AND name */     --animdur: 4s;     --animn: opacity;   }   [data-animation="slow"] {     /* Change animation duration */     --animdur: 10s;   }   [data-animation="stop"] {     /* Stop the animation */     --animps: paused;   } }

If this is too generic, and you prefer having unique, alternate animations per animation class, group the selectors like this:

.a-slide[data-animation="alternate"] { /* etc. */ }

Here’s a Pen with a checkbox simulating prefers-reduced-motion. Scroll down within the Pen to see the behavior change for each circle:

Pausing with JavaScript

To re-create the “Pause all animations”-checkbox in JavaScript, iterate all the [data-animation]-elements and toggle the same --animps custom property:

<button id="js-toggle" type="button">Toggle Animations</button>
const animations = document.querySelectorAll('[data-animation'); const jstoggle = document.getElementById('js-toggle');  jstoggle.addEventListener('click', () => {   animations.forEach(animation => {     const running = getComputedStyle(animation).getPropertyValue("--animps") || 'running';     animation.style.setProperty('--animps', running === 'running' ? 'paused' : 'running');   }) });

It’s exactly the same concept as the checkbox hack, using the same custom property: --animps, only set by JavaScript instead of CSS. If we want to support older browsers, we can toggle a class, that will update the animation-play-state.

Using IntersectionObserver

To play and pause all [data-animation]-animations automatically — and thus not unnecessarily overloading the GPU — we can use an IntersectionObserver.

First, we need to make sure that no animations are running at all:

[data-animation] {   /* Change 'running' to 'paused' */   animation: var(--animps, paused);  }

Then, we’ll create the observer and trigger it when an element is 25% or 75% in viewport. If the latter is matched, the animation starts playing; otherwise it pauses.

By default, all elements with a [data-animation]-attribute will be observed, but if prefers-reduced-motion is enabled (set to “reduce”), the elements with [data-animation="stop"] will be ignored.

const IO = new IntersectionObserver((entries) => {   entries.forEach((entry) => {     if (entry.isIntersecting) {       const state = (entry.intersectionRatio >= 0.75) ? 'running' : 'paused';       entry.target.style.setProperty('--animps', state);     }   }); }, {   threshold: [0.25, 0.75] });  const mediaQuery = window.matchMedia("(prefers-reduced-motion: reduce)"); const elements = mediaQuery?.matches ? document.querySelectorAll(`[data-animation]:not([data-animation="stop"]`) : document.querySelectorAll('[data-animation]');  elements.forEach(animation => {   IO.observe(animation); });

You have to play around with the threshold-values, and/or whether you need to unobserve some animations after they’ve triggered, etc. If you load new content or animations dynamically, you might need to re-write parts of the observer as well. It’s impossible to cover all scenarios, but using this as a foundation should get you started with auto-playing and pausing CSS animations!

Bonus: Adding <audio> to the slideshow with minimal JavaScript

Here’s an idea to add music to the slideshow we built. First, add an audio-tag:

<audio src="/asset/audio/slideshow.mp3" hidden loop></audio>

Then, in Javascript:

const audio = document.querySelector('your-audio-selector'); const details = document.querySelector('your-details-selector'); details.addEventListener('toggle', () => {   details.open ? audio.play() : audio.pause(); })

Pretty simple, huh?

I did a “Silent Movie” (with audio)-demo here, where you get to know my geeky past. 🙂


The post How to Play and Pause CSS Animations with CSS Custom Properties appeared first on CSS-Tricks.

You can support CSS-Tricks by being an MVP Supporter.

CSS-Tricks

, , , ,
[Top]

Svelte and Spring Animations

Spring animations are a wonderful way to make UI interactions come to life. Rather than merely changing a property at a constant rate over a period of time, springs allow us to move things using spring physics, which gives the impression of a real thing moving, and can appear more natural to users.

I’ve written about spring animations previously. That post was based on React, using react-spring for the animations. This post will explore similar ideas in Svelte.

CSS devs! It’s common to think of easing when it comes to controling the feel of animations. You could think of “spring” animations as a subcategory of easing that are based on real-world physics.

Svelte actually has springs built into the framework, without needing any external libraries. We’ll rehash what was covered in the first half my previous post on react-spring. But after that, we’ll take a deep-dive into all the ways these springs can be used with Svelte, and leave the real world implementation for a future post. While that may seem disappointing, Svelte has a number of wonderful, unique features with no counterpart in React, which can be effectively integrated with these animation primitives. We’re going to spend some time talking about them.

One other note: Some of the demos sprinkled throughout may look odd because I configured the springs to be extra “bouncy” to create more obvious effect. If you the code for any of them, be sure to find a spring configuration that works for you.

Here’s a wonderful REPL Rich Harris made to show all the various spring configurations, and how they behave.

A quick primer on Svelte Stores

Before we start, let’s take a very, very quick tour of Svelte stores. While Svelte’s components are more than capable of storing and updating state, Svelte also has the concept of a store, which allows you to store state outside of a component. Since Svelte’s Spring API uses Stores, we’ll quickly introduce the salient parts here.

To create an instance of a store, we can import the writable type, and create it like so:

import { writable } from "svelte/store"; const clicks = writable(0);

The clicks variable is a store that has a value of 0. There’s two ways to set a new value of a store: the set and update methods. The former receives the value to which you’re setting the store, while the latter receives a callback, accepting the current value, and returning the new value.

function increment() {   clicks.update(val => val + 1); } function setTo5() {   clicks.set(5); }

State is useless if you can’t actually consume it. For this, stores offer a subscribe method, which allows you to be notified of new values — but when using it inside of a component, you can prefix the store’s name with the $ character, which tells Svelte to not only display the current value of the store, but to update when it changes. For example:

<h1>Value {$  clicks}</h1> <button on:click={increment}>Increment</button> <button on:click={setTo5}>Set to 5</button>

Here’s a full, working example of this code. Stores offer a number of other features, such as derived stores, which allow you to chain stores together, readable stores, and even the ability to be notified when a store is first observed, and when it no longer has observers. But for the purposes of this post, the code shown above is all we need to worry about. Consult the Svelte docs or interactive tutorial for more info.

A crash course on springs

Let’s walk through a quick introduction of springs, and what they accomplish. We’ll take a look at a simple UI that changes a presentational aspect of some elements — opacity and transform — and then look at animating that change.

This is a minimal Svelte component that toggles the opacity of one <div>, and toggles the x-axis transform of another (without any animation).

<script>   let shown = true;   let moved = 0;    const toggleShow = () => (shown = !shown);   const toggleMove = () => (moved = moved ? 0 : 500); </script>  <div style="opacity: {shown ? 1 : 0}">Content to toggle</div> <br /> <button on:click={toggleShow}>Toggle</button> <hr /> <div class="box" style="transform: translateX({moved}px)">I'm a box.</div> <br /> <button on:click={toggleMove}>Move it!</button>

These changes are applied instantly, so let’s look at animating them. This is where springs come in. In Svelte, a spring is a store that we set the desired value on, but instead of instantly changing, the store internally uses spring physics to gradually change the value. We can then bind our UI to this changing value, to get a nice animation. Let’s see it in action.

<script>   import { spring } from "svelte/motion";    const fadeSpring = spring(1, { stiffness: 0.1, damping: 0.5 });   const transformSpring = spring(0, { stiffness: 0.2, damping: 0.1 });    const toggleFade = () => fadeSpring.update(val => (val ? 0 : 1));   const toggleTransform = () => transformSpring.update(val => (val ? 0 : 500));   const snapTransform = () => transformSpring.update(val => val, { hard: true }); </script>  <div style="opacity: {$  fadeSpring}">Content to fade</div> <br /> <button on:click={toggleFade}>Fade Toggle</button>  <hr />  <div class="box" style="transform: translateX({$  transformSpring}px)">I'm a box.</div> <br /> <button on:click={toggleTransform}>Move it!</button> <button on:click={snapTransform}>Snap into place</button>

We get our spring function from Svelte, and set up different spring instances for our opacity, and transform animations. The transform spring config is purposefully set up to be extra springy, to help show later how we can temporarily turn off spring animations, and instantly apply desired changes (which will come in handy later). At the end of the script block are our click handlers for setting the desired properties. Then, in the HTML, we bind our changing values directly to our elements… and that’s it! That’s all there is to basic spring animations in Svelte.

The only remaining item is the snapTransform function, where we set our transform spring to its current value, but also pass an object as the second argument, with hard: true. This has the effect of immediately applying the desired value with no animation at all.

This demo, as well as the rest of the basic examples we’ll look at in this post, is here:

Animating height

Animating height is trickier than other CSS properties, since we have to know the actual height to which we’re animating. Sadly, we can’t animate to a value of auto. That wouldn’t make sense for a spring, since the spring needs a real number so it can interpolate the correct values via spring physics. And as it happens, you can’t even animate auto height with regular CSS transitions. Fortunately, the web platform gives us a handy tool for getting the height of an element: a ResizeObserver, which enjoys pretty good support among browsers.

Let’s start with a raw height animation of an element, producing a “slide down” effect that we gradually refine in other examples. We’ll be using ResizeObserver to bind to an element’s height. I should note that Svelte does have an offsetHeight binding that can be used to more directly bind an element’s height, but it’s implemented with some <iframe> hacks that cause it to only work on elements that can receive children. This would probably be good enough for most use cases, but I’ll use a ResizeObserver because it allows some nice abstractions in the end.

First, we’ll bind an element’s height. It’ll receive the element and return a writable store that initializes a ResizeObserver, which updates the height value on change. Here’s what that looks like:

export default function syncHeight(el) {   return writable(null, (set) => {     if (!el) {       return;     }     let ro = new ResizeObserver(() => el && set(el.offsetHeight));     ro.observe(el);     return () => ro.disconnect();   }); }

We’re starting the store with a value of null, which we’ll interpret as “haven’t measured yet.” The second argument to writable is called by Svelte when the store becomes active, which it will be as soon as it’s used in a component. This is when we fire up the ResizeObserver and start observing the element. Then, we return a cleanup function, which Svelte calls for us when the store is no longer being used anywhere.

Let’s see this in action:

<script>   import syncHeight from "../syncHeight";   import { spring } from "svelte/motion";    let el;   let shown = false;   let open = false;   let secondParagraph = false;    const heightSpring = spring(0, { stiffness: 0.1, damping: 0.3 });   $  : heightStore = syncHeight(el);   $  : heightSpring.set(open ? $  heightStore || 0 : 0);    const toggleOpen = () => (open = !open);   const toggleSecondParagraph = () => (secondParagraph = !secondParagraph); </script>  <button on:click={ toggleOpen }>Toggle</button> <button on:click={ toggleSecondParagraph }>Toggle More</button> <div style="overflow: hidden; height: { $  heightSpring }px">   <div bind:this={el}>     <div>...</div>     <br />     {#if secondParagraph}     <div>...</div>     {/if}   </div> </div>

Our el variable holds the element we’re animating. We tell Svelte to set it to the DOM element via bind:this={el}. heightSpring is our spring that holds the height value of the element when it’s open, and zero when it’s closed. Our heightStore is what keeps it up to date with the element’s current height. el is initially undefined, and syncHeight returns a junk writable store that basically does nothing. As soon as el is assigned to the <div> node, that line will re-fire — thanks to the $ : syntax — and get our writable store with the ResizeObserver listening.

Then, this line:

$  : heightSpring.set(open ? $  heightStore || 0 : 0);

…listens for changes to the open value, and also changes to the height value. In either case, it updates our spring store. We bind the height in HTML, and we’re done!

Be sure to remember to set overflow to hidden on this outer element so the contents are properly clipped as the elements toggles between its opened and closed states. Also, changes to the element’s height also animate into place, which you can see with the “Toggle More” button. You can run this in the embedded demo in the previous section.

Note that this line above:

$  : heightStore = syncHeight(el);

…currently causes an error when using server-side rendering (SSR), as explained in this bug. If you’re not using SSR you don’t need to worry about it, and of course by the time you read this that bug may have been fixed. But the workaround is to merely do this:

let heightStore; $  : heightStore = syncHeight(el);

…which works but is hardly ideal.

We probably don’t want the <div> to spring open on first render. Also, the opening spring effect is nice, but when closing, the effect is janky due to some content flickering. We can fix that. To prevent our initial render from animating, we can use the { hard: true } option we saw earlier. Let’s change our call to heightSpring.set to this:

$  : heightSpring.set(open ? $  heightStore || 0 : 0, getConfig($  heightStore));

…and then see about writing a getConfig function that returns an object with the hard property that was set to true for the first render. Here’s what I came up with:

let shown = false;  const getConfig = val => {   let active = typeof val === "number";   let immediate = !shown && active;   //once we've had a proper height registered, we can animate in the future   shown = shown || active;   return immediate ? { hard: true } : {}; };

Remember, our height store initially holds null and only gets a number when the ResizeObserver starts running. We capitalize on this by checking for an actual number. If we have a number, and we haven’t yet shown anything, then we know to show our content immediately, and we we do that by setting the immediate value. That value ultimately triggers the hard config value in the spring, which we saw before.

Now let’s tweak the animation to be a bit less, well, springy when we close our content. That way, things won’t flicker when they close. When we initially created our spring, we specified stiffness and damping, like so

const heightSpring = spring(0, { stiffness: 0.1, damping: 0.3 });

It turns out the spring object itself maintains those properties, which can be set anytime. Let’s update this line:

$  : heightSpring.set(open ? $  heightStore || 0 : 0, getConfig($  heightStore));

That detects changes to the open value (and the heightStore itself) to update the spring. Let’s also update the spring’s settings based on whether we’re opening or closing. Here’s what it looks like:

$  : {   heightSpring.set(open ? $  heightStore || 0 : 0, getConfig($  heightStore));   Object.assign(     heightSpring,     open ? { stiffness: 0.1, damping: 0.3 } : { stiffness: 0.1, damping: 0.5 }   ); }

Now when we get a new open or height value, we call heightSpring.set just like before, but we also set stiffness and damping values on the spring that are applied based on whether the element is open. If it’s closed, we set damping up to 0.5, which reduces the springiness. Of course, you’re welcome to tweak all these values and configure them as you’d like! You can see this in the “Animate Height Different Springs” section of the demo.

You might notice our code is starting to grow pretty quickly. We’ve added a lot of boilerplate to cover some of these use cases, so let’s clean things up. Specifically, we’ll make a function that creates our spring and that also exports a sync function to handle our spring config, initial render, etc.

import { spring } from "svelte/motion";  const OPEN_SPRING = { stiffness: 0.1, damping: 0.3 }; const CLOSE_SPRING = { stiffness: 0.1, damping: 0.5 };  export default function getHeightSpring() {   const heightSpring = spring(0);   let shown = false;    const getConfig = (open, val) => {     let active = typeof val === "number";     let immediate = open && !shown && active;     // once we've had a proper height registered, we can animate in the future     shown = shown || active;     return immediate ? { hard: true } : {};   };    const sync = (open, height) => {     heightSpring.set(open ? height || 0 : 0, getConfig(open, height));     Object.assign(heightSpring, open ? OPEN_SPRING : CLOSE_SPRING);   };    return { sync, heightSpring }; }

There’s a lot of code here, but it’s all the code we’ve been writing so far, just packaged into a single function. Now our code to use this animation is simplified to just this

const { heightSpring, sync } = getHeightSpring(); $  : heightStore = syncHeight(el); $  : sync(open, $  heightStore);

You can see in the “Animate Height Cleanup” section of the demo.

Some Svelte-specific tricks

Let’s pause for a moment and consider some ways Svelte differs from React, and how we might leverage that to improve what we have even further.

First, the stores we’ve been using to hold springs and change height values are, unlike React’s hooks, not tied to component rendering. They’re plain JavaScript objects that can be consumed anywhere. And, as alluded to above, we can imperatively subscribe to them so that they manually observe changing values.

Svelte also something called actions. These are functions that can be added to a DOM element. When the element is created, Svelte calls the function and passes the element as the first argument. We can also specify additional arguments for Svelte to pass, and provide an update function for Svelte to re-run when those values change. Another thing we can do is provide a cleanup function for Svelte to call when it destroys the element.

Let’s put these tools together in a single action that we can simply drop onto an element to handle all the animation we’ve been writing so far:

export default function slideAnimate(el, open) {   el.parentNode.style.overflow = "hidden";    const { heightSpring, sync } = getHeightSpring();   const doUpdate = () => sync(open, el.offsetHeight);   const ro = new ResizeObserver(doUpdate);    const springCleanup = heightSpring.subscribe((height) => {     el.parentNode.style.height = `$  { height }px`;   });    ro.observe(el);    return {     update(isOpen) {       open = isOpen;       doUpdate();     },     destroy() {       ro.disconnect();       springCleanup();     }   }; }

Our function is called with the element we want to animate, as well as the open value. We’ll set the element’s parent to have overflow: hidden. Then we use the same getHeightSpring function from before, set up our ResizeObserver, etc. The real magic is here.

const springCleanup = heightSpring.subscribe((height) => {   el.parentNode.style.height = `$  {height}px`; });

Instead of binding our heightSpring to the DOM, we manually subscribe to changes, then set the height ourselves, manually. We wouldn’t normally do manual DOM updates when using a JavaScript framework like Svelte but, in this case, it’s for a helper library, which is just fine in my opinion.

In the object we’re returning, we define an update function which Svelte will call when the open value changes. We update the original argument to this function, which the function closes over ( i.e. creates a closure around) and then calls our update function to sync everything. Svelte calls the destroy function when our DOM node is destroyed.

Best of all, using this action is a snap:

<div use:slideAnimate={open}>

That’s it. When open changes, Svelte calls our update function.

Before we move on, let’s make one other tweak. Notice how we remove the springiness by changing the spring config when we collapse the pane with the “Toggle” button; however, when we make the element smaller by clicking the “Toggle More” button, it shrinks with the usual springiness. I dislike that, and prefer shrinking sizes move with the same physics we’re using for collapsing.

Let’s start by removing this line in the getHeightSpring function:

Object.assign(heightSpring, open ? OPEN_SPRING : CLOSE_SPRING);

That line is inside the sync function that getHeightSpring created, which updates our spring settings on every change, based on the open value. With it gone, we can start our spring with the “open” spring config:

const heightSpring = spring(0, OPEN_SPRING);

Now let’s change our spring settings when either the height of our content changes, or when the open value changes. We already have the ability to observe both of those things changing — our ResizeObserver callback fires when the size of the content changes, and the update function of our action fires whenever open changes.

Our ResizeObserver callback can be changed, like this:

let currentHeight = null; const ro = new ResizeObserver(() => {   const newHeight = el.offsetHeight;   const bigger = newHeight > currentHeight;    if (typeof currentHeight === "number") {     Object.assign(heightSpring, bigger ? OPEN_SPRING : CLOSE_SPRING);   }   currentHeight = newHeight;   doUpdate(); });

currentHeight holds the current value, and we check it on size changes to see which direction we’re moving. Next up is the update function. Here’s what it looks like after our change:

update(isOpen) {   open = isOpen;   Object.assign(heightSpring, open ? OPEN_SPRING : CLOSE_SPRING);   doUpdate(); },

Same idea, but now we’re only checking whether open is true or false. You can see these iterations in the “Slide Animate” and “Slide Animate 2” sections of the demo.

Transitions

We’ve talked about animating items already on the page so far, but what about animating an object when it first renders? And when it un-mounts? That’s called a transition, and it’s built into Svelte. The docs do a superb job covering the common use cases, but there’s one thing that’s not yet (directly) supported: spring-based transitions.

/explanation Note that what Svelte calls a “transition” and what CSS calls a “transition” are very different things. CSS means transitioning one value to another. Svelte is referring to elements as they “transition” into and out of the DOM entirely (something that CSS doesn’t help with much at all).

To be clear, the work we’re doing here is made for adding spring-based animations into Svelte’s transitions. This is not currently supported, so it requires some tricks and workarounds that we’ll get into. If you don’t care about using springs, then Svelte’s built-in transitions can be used, which are significantly simpler. Again, check the docs for more info.

The way transitions work in Svelte is that we provide a duration in milliseconds (ms) along with an optional easing function, then Svelte provides us a callback with a value running from 0 to 1, representing how far along the transition is, and we turn that into whatever CSS we want. For example:

const animateIn = () => {   return {     duration: 2000,     css: t => `transform: translateY($  {t * 50 - 50}px)`   }; };

…is used like this:

<div in:animateIn out:animateOut class="box">   Hello World! </div>

When that <div> first mounts, Svelte:

  • calls our animateIn function,
  • rapidly calls the CSS function on our resulting object ahead of time with values from 0 to 1,
  • collects our changing CSS result, then
  • compiles those results into a CSS keyframes animation, which it then applies to the incoming <div>.

This means that our animation will run as a CSS animation — not as JavaScript on the main thread — offering a nice performance boost for free.

The variable t starts at 0, which results in a translation of -50px. As t gets closer to 1, the translation approaches 0, its final value. The out transition is about the same, but in reverse, with the added feature of detecting the box’s current translation value, starting from there. So, if we add it then quickly remove it, the box will start to leave from its current position rather than jumping ahead. However, if we then re-add it while it’s leaving, it will jump, something we’ll talk about in just moment.

You can run this in the “Basic Transition” section of the demo.

Transitions, but with springs

While there’s a number of easing functions that alter the flow of an animation, there’s no ability to directly use springs. But what we could do is find some way to run a spring ahead of time, collect the resulting values, and then, when our css function is called with the a t value running from 0 to 1, look up the right spring value. So, if t is 0, we obviously need the first value from thespring. When t is 0.5, we want the value right in the middle, and so on. We also need a duration, which is number_of_spring_values * 1000 / 60 since there’s 60 frames per second.

We won’t write that code here. Instead, we’ll use the solution that already exists in the svelte-helpers library, a project I started. I grabbed one small function from the Svelte codebase, spring_tick, then wrote a separate function to repeatedly call it until it’s finished, collecting the values along the way. That, along with a translation from t to the correct element in that array (or a weighted average if there’s not a direct match), is all we need. Rich Harris gave a helping hand on the latter, for which I’m grateful.

Animate in

Let’s pretend a big red <div> is a modal that we want to animate in, and out. Here’s what an animateIn function looks like:

import { springIn, springOut } from "svelte-helpers/animation"; const SPRING_IN = { stiffness: 0.1, damping: 0.1 };  const animateIn = node => {   const { duration, tickToValue } = springIn(-80, 0, SPRING_IN);   return {     duration,     css: t => `transform: translateY($  { tickToValue(t) }px)`   }; };

We feed the values we want to spring to, as well as our spring config to the springIn function. That gives us a duration, and a function for translating the current tickToValue into the current value to apply in the CSS. That’s it!

Animate out

Closing the modal is the same thing, with one small tweak

const SPRING_OUT = { stiffness: 0.1, damping: 0.5, precision: 3 };  const animateOut = node => {   const current = currentYTranslation(node);   const { duration, tickToValue } = springOut(current ? current : 0, 80, SPRING_OUT);   return {     duration: duration,     css: t => `transform: translateY($  { tickToValue(t) }px)`   }; };

Here, we’re check the modal’s current translation position, then use that as a starting point for the animation. This way, if the user opens and then quickly closes the modal, it’ll exit from its current position, rather than teleporting to 0, and then leaving. This works because the animateOut function is called when the element un-mounts, at which point we generate the object with the duration property and css function so the animation can be computed.

Sadly, it seems re-mounting the object while it’s in the process of leaving does not work, at least well. The animateIn function is not called de novo, but rather the original animation is re-used, which means it’ll always start at -80. Fortunately this almost certainly would not matter for a typical modal component, since a modal is usually removed by clicking on something, like the background overlay, meaning we are unable to re-show it until that overlay has finished animating out. Besides, repeatedly adding and removing an element with bidirectional transitions might make for a fun demo, but they’re not really common in practice, at least in my experience.

One last quick note on the outgoing spring config: You may have noticed that I set the precision ridiculously high (3 when the default is 0.01). This tells Svelte how close to get to the target value before deciding it is “done.” If you leave the default at 0.01, the modal will (almost) hit its destination, then spend quite a few milliseconds imperceptibly getting closer and closer before deciding it’s done, then remove itself from the DOM. This gives the impression that the modal is stuck, or otherwise delayed. Moving the precision to a value of 3 fixes this. Now the modal animates to where it should go (or close enough), then quickly goes away.

More animation

Let’s add one final tweak to our modal example. Let’s have it fade in and out while animating. We can’t use springs for this, since, again, we need to have one canonical duration for the transition, and our motion spring is already providing that. But spring animations usually make sense for items actually moving, and not much else. So let’s use an easing function to create a fade animation.

If you need help picking the right easing function, be sure to check out this handy visualization from the Svelte docs. I’ll be using the quintOut and quadIn functions.

import { quintOut, quadIn } from "svelte/easing";

Our new animateIn function looks pretty similar. Our css function does what it did before, but also runs the tickToValue value through the quintOut easing function to get our opacity value. Since t runs from 0 to 1 during an in transition, and 1 to 0 during an out transition, we don’t have to do anything further to it before applying to opacity.

const SPRING_IN = { stiffness: 0.1, damping: 0.1 }; const animateIn = node =>; {   const { duration, tickToValue } = springIn(-80, 0, SPRING_IN);   return {     duration,     css: t => {       const transform = tickToValue(t);       const opacity = quintOut(t);       return `transform: translateY($  { transform }px); opacity: $  { opacity };`;     }   }; };

Our animateOut function is similar, except we want to grab the element’s current opacity value, and force the animation to start there. So, if the element is in the process of fading in, with an opacity of, say, 0.3, we don’t want to reset it to 1, and then fade it out. Instead, we want to fade it out from 0.3.

Multiplying that starting opacity by whatever value the easing function returns accomplishes this. If our t value starts at 1, then 1 * 0.3 is 0.3. If t is 0.95, we do 0.95 * 0.3 to get a value, which is a little less than 0.3, and so on.

Here’s the function:

const animateOut = node => {   const currentT = currentYTranslation(node);   const startOpacity = +getComputedStyle(node).opacity;   const { duration, tickToValue } = springOut(     currentT ? currentT : 0,     80,     SPRING_OUT   );   return {     duration,     css: t => {       const transform = tickToValue(t);       const opacity = quadIn(t);       return `transform: translateY($  { transform }px); opacity: $  { startOpacity * opacity }`;     }   }; }; 

You can run this example in the demo with the “Spring Transition With Fade component.

Parting thoughts

Svelte is a lot of fun! In my (admittedly limited) experience, it tends to provide extremely simple primitives, and then leaves you to code up whatever you need. I hope this post has helped explain how the spring animations can be put to good use in your web applications.

And, hey, just a quick reminder to consider accessibility when working with springs, just as you would do with any other animation. Pairing these techniques with something like prefers-reduced-motion can ensure that only folks who prefer animations are the ones who get them.


The post Svelte and Spring Animations appeared first on CSS-Tricks.

You can support CSS-Tricks by being an MVP Supporter.

CSS-Tricks

, ,
[Top]

Libraries for SVG Drawing Animations

In 2013, Jake Archibald introduced this cool trick of animating an SVG path to look like it’s drawing itself. It’s 2020 now, and the trick is still popular. I’ve seen it on a lot of websites I’ve visited recently. I, too, feature an animated SVG loader on my website using one of the libraries I’ll introduce below.

In a previous article, Chris Coyier wrote about how SVG path animations work under the hood, using the CSS stroke-dasharray and stroke-dashoffset properties. In this article, I want to introduce you to four JavaScript libraries that can be used to create SVG path drawing animations with fewer lines of code, like this cool example. Why a library? Because they’re ideal for complex animations involving two or more SVGs with multiple paths.

To get started, l’ll first secure an SVG to demo. Let’s use this castle from svgrepo. The castle SVG downloads as an SVG image. But, since we’re dealing with path animation, what we need is the code format of the SVG. To get this, I’ll import the file into Figma and use the “Copy as SVG” feature (Right Click → Copy/Paste → Copy as SVG) to grab the SVG code.

To successfully animate an SVG path, the SVG shape should have a fill of none and each individual SVG path must have a stroke (we’ll set it to #B2441D) and a stroke-width (set to 2px).

The animation effect we want to create is to first draw the outline (or stroke) of the SVG and then fill in the different colors. In total, there are six different fill colors used throughout the SVG, so we’ll remove the fill color from each path and give paths of the same color the same class name.

  • #695A69: color-1
  • #B2441D: color-2
  • #DFDOC6: color-3
  • #C8B2A8: color-4
  • #DE582A: color-5
  • #AO8A8A: color-6

After all the modifications, here’s what the SVG code looks like:

 <svg id="svg-castle" width="480" height="480" viewBox="0 0 480 480" fill="none" xmlns="http://www.w3.org/2000/svg">   <path d="M231.111 183.761V150.371C231.111 149.553 231.774 148.889 232.592 148.889H24  7.407C248.225 148.889 248.889 149.552 248.889 150.371V183.761L258.342 206.667H271.111  V135.556H240H208.889V206.667H221.658L231.111 183.761Z" stroke="#B2441D" stroke-width="2px" class="color-6" />   <path d="M311.111 420H288.889V455.556V468.889H311.111V455.556V420Z" stroke="#B2441D"   stroke-width="2px" class="color-1" />   <path d="M191.111 420H168.889V455.556V468.889H191.111V455.556V420Z" stroke="#B2441D" stroke-width="2px" class="color-1" />   <path d="M168.889 220V228.889V237.778H222.222V228.889H212.487L221.658 206.667H208.88   9H169.524L177.778 220H168.889Z" stroke="#B2441D" stroke-width="2px" class="color-2"/ >   <!-- etc. --> </svg>

That’s all the SVG preparation we need. Let’s look at how to achieve the desired animation with the different libraries.

Library 1: Vivus

Vivus is a lightweight JavaScript class (with no dependencies) that allows you to animate SVGs like they’re being drawn. The library is available using any of these options. To keep things simple, we’ll use a CDN link:

<script src="https://cdnjs.cloudflare.com/ajax/libs/vivus/0.4.5/vivus.min.js" integrity="sha512-NBLGIjYyAoYAr23l+dmAcUv7TvFj0XrqZoFa4i1o+F2VvF9SrERyMD8BHNnJn1SEGjl1AouBDcCv/q52L3ozBQ==" crossorigin="anonymous"></script>

Next, let’s create a new Vivus instance. It takes three arguments:

  1. The ID of the target element (the SVG)
  2. An options object with a dozen possible values
  3. A callback function that runs at the end of the animation

Looking back at our SVG code, the SVG ID is svg-castle.

new Vivus('svg-castle', {    duration: 200, type:'oneByOne' });

Now, let’s write a callback function that fills the paths with the different colors we’ve defined:

function fillPath(classname, color) {   const paths = document.querySelectorAll(`#svg-castle .$ {classname}`);   for (path of paths){     path.style.fill = `$ {color}`;   } }

The fillPath function selects all paths in the svg-castle element with the supplied classname, loops through and fills each path with the specified color. Remember in a previous step, we removed the fill from each path and gave each path a same fill class (color-1, color-2, etc.).

Next up, we call the fillPath function for the six different classnames and their corresponding colors:

function after() {   fillPath('color-1', '#695a69');   fillPath('color-2', '#b2441d');   fillPath('color-3', '#dfd0c6');   fillPath('color-4', '#c8b2a8');   fillPath('color-5', '#de582a');   fillPath('color-6', '#a08a8a') }

That’s the callback function passed to the Vivus instance. See Pen for full implementation.

Library 2: Walkway.js

Walkway is a light-weight SVG animation library for path, line and polygon elements. To start using it, we can either add the library using npm, yarn, or with a CDN link like we did with Vivus. We’ll go with the CDN link once again:

<script src="https://cdn.jsdelivr.net/npm/walkway.js/src/walkway.min.js"></script>

With Walkway, we create a new Walkway instance, passing an options object as an argument. Then, we call the draw method on the new instance and pass in an optional callback function which will be run at the end of the draw animation. Again, very much like Vivus.

We’ve already written the after callback function in the previous example, so the rest should be a piece of cake:

const svg = new Walkway({   selector: '#svg-castle',   duration: 3000, });  svg.draw(after);

Library 3: Lazy Line Painter

Lazy Line Painter is a modern JavaScript library for SVG path animation. It requires minimal code to setup. However, if a GUI is more of your thing, you can use the Lazy Line Composer which is a free online editor for SVG path animation from the same makers. The SVG will be exported as an animated SVG file that can be used directly anywhere.

The basic setup for Lazy Line Painter is similar to what we’ve already done in the other examples. First, get the library using either npm or a CDN link. Just like the previous examples, we’ll use a CDN link:

<script src="https://cdn.jsdelivr.net/npm/lazy-line-painter@1.9.4/lib/lazy-line-painter-1.9.4.min.js"></script>

Then, we initialize a new LazyLinePainter instance, which accepts two parameters — a selector (the ID of the target SVG element) and a config object. Let’s call the paint method on the new instance:

// select the svg by id let svg = document.querySelector('#svg-castle')  // define config options let options = {   strokeDash: '2, 2', } // initialize new LazyLinePainter instance let myAnimation = new LazyLinePainter(svg, options)  // call the paint method myAnimation.paint()

A full list of config options are available in the library docs. Unlike the previous libraries, we don’t pass a callback function to the paint method. Instead, we’ll listen for the complete:all event handler on the animation and then pass in the callback function.

myAnimation.on('complete:all', (event) => {after()});

We can also control when the paint method runs using event listeners like we’ve have done in the following codepen demo. Click on the castle to re-run the animation.

Library 4: Framer Motion

Framer Motion is a bit different from other libraries we’ve covered. It’s a production-ready open-source animation library for React components with tons of possible animation types. And, yes, this is from the same team behind the popular Framer prototyping tool.

First up, we’ll install the library with npm in the terminal:

npm install framer-motion

For SVG path drawing animations, Framer Motion provides a motion.path component that takes four props:

<motion.path   d={pathDefinition}   initial={{ pathLength: 1, pathOffset: 0 }}   animate={{ pathLength: 0, pathOffset: 1 }}   transition={{ duration: 2 }} />

To use it, we’ll simply convert our SVG paths to motion.path, like this:

import React from 'react'; import { motion } from "framer-motion"; const AnimatedCastle = () => {   return (     <svg id="svg-castle" width="480" height="480" viewBox="0 0 480 480" fill="non            e" xmlns="http://www.w3.org/2000/svg">       <motion.path d="M311.111 420H288.889V455.556V468.889H311.111V455.556V420Z"              stroke="#B2441D" stroke-width="2" className="color-1"        initial={{ pathLength: 1,fill:"none", opacity:0, }}        animate={{ pathLength: 0,fill:"695A69", opacity:1 }}        transition={{ duration: 2 }}       />       <motion.path d="M191.111 420H168.889V455.556V468.889H191.111V455.556V420Z"                stroke="#B2441D" stroke-width="2" className="color-2"         initial={{ pathLength: 1, fill:"none", opacity:0, }}         animate={{ pathLength: 0, fill:"#b2441d", opacity:1}}         transition={{ duration: 3 }}       />                 <!-- etc. -->     </svg>   ) }

This has to be done for each SVG path. See this demo for full implementation:

There’s a caveat though: the castle SVG has over 60 paths, which is a lot. Going through them was quite daunting for me, and I found the process to be repetitive and prone to errors. For that reason, I don’t recommend Framer Motion but I would say that it is well suited for SVGs within React components with no more than five paths. For anything more than that, go with any of the previous libraries we covered.

Conclusion

That’s a look at four JavaScript libraries we can use to get hand-drawn SVG effects.

Why didn’t we cover a CSS-only solution? While it’s possible to do, it involves a lot of code repetition. For example, it means finding the total length of each path using JavaScript or with this cool trick that sets each path length to 1, and then sets the stroke-dasharrray and stroke-dashoffset of each path to its path length.

After that, we still need to define keyframes to animate the stroke-dashoffset to zero. Then, those keyframe animations will be added to each path and with an animation-delay to offset things a bit. We also have to write six different keyframe rules to fill the paths with their respective colors. Considering that the castle has over 60 individual paths, that’s over 100 lines of CSS! Not exactly the most efficient or straightforward approach.


The post Libraries for SVG Drawing Animations appeared first on CSS-Tricks.

You can support CSS-Tricks by being an MVP Supporter.

CSS-Tricks

, ,
[Top]