Archive

Author Archive

You can use text-wrap: balance; on icons

October 24th, 2024 No comments

Terence Eden on using text-wrap: balance for more than headings:

But the name is, I think, slightly misleading. It doesn’t only work on text. It will work on any content. For example – I have a row of icons at the bottom of this page. If the viewport is too narrow, a single icon might drop to the next line. That can look a bit weird.

Heck yeah. I may have reached for some sort of auto-fitting grid approach, but hey, may as well go with a one-liner if you can! And while we’re on the topic, I just wanna mention that, yes, text-wrap: balance will work on any content. — just know that the spec is a little opinionated on this and make sure that the content is fewer than five lines.

There’s likely more nuance to come if the note for Issue 6 in the spec is any indication about possibly allowing for a line length minimum:

Suggestion for value space is match-indent | |  (with Xch given as an example to make that use case clear). Alternately  could actually count the characters.


You can use text-wrap: balance; on icons originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

Categories: Designing, Others Tags:

Clarifying the Relationship Between Popovers and Dialogs

October 23rd, 2024 No comments
Line diagram connecting the popover attribute to six types of accessible roles, including dialog.

The difference between Popovers (i.e., the popover attribute) and Dialogs (i.e., both the element and the dialog accessible role) is incredibly confusing — so much that many articles (like this, this, and this) have tried to shed some light on the issue.

If you’re still feeling confused, I hope this one clears up that confusion once and for all.

Distinguishing Popovers From Dialogs

Let’s pull back on the technical implementations and consider the greater picture that makes more sense and puts everything into perspective.

The reason for this categorization comes from a couple of noteworthy points.

First, we know that a popover is content that “pops” up when a user clicks a button (or hovers over it, or focuses on it). In the ARIA world, there is a useful attribute called aria-haspopup that categorizes such popups into five different roles:

  • menu
  • listbox
  • tree
  • grid
  • dialog

Strictly speaking, there’s a sixth value, true, that evaluates to menu. I didn’t include it above since it’s effectively just menu.

By virtue of dialog being on this list, we already know that dialog is a type of popover. But there’s more evidence behind this too.

The Three Types of Dialogues

Since we’re already talking about the dialog role, let’s further expand that into its subcategories:

Lone diagram connecting the popover attribute to six accessible roles, including dialog, which is broken out into three categories.

Dialogs can be categorized into three main kinds:

  • Modal: A dialog with an overlay and focus trapping
  • Non-Modal: A dialog with neither an overlay nor focus trapping
  • Alert Dialog: A dialog that alerts screen readers when shown. It can be either modal or non-modal.

This brings us to another reason why a dialog is considered a popover.

Some people may say that popovers are strictly non-modal, but this seems to be a major misunderstanding — because popovers have a ::backdrop pseudo-element on the top layer. The presence of ::backdrop indicates that popovers are modal. Quoting the CSS-Tricks almanac:

The ::backdrop CSS pseudo-element creates a backdrop that covers the entire viewport and is rendered immediately below a , an element with the popup attribute, or any element that enters fullscreen mode using the Fullscreen API.

That said, I don’t recommend using the Popover API for modality because it doesn’t have a showModal() method (that has) that creates inertness, focus trapping, and other necessary features to make it a real modal. If you only use the Popover API, you’ll need to build those features from scratch.

So, the fact that popovers can be modal means that a dialog is simply one kind of popover.

A Popover Needs an Accessible Role

Popovers need a role to be accessible. Hidde has a great article on selecting the right role, but I’m going to provide some points in this article as well.

To start, you can use one of the aria-haspopup roles mentioned above:

  • menu
  • listbox
  • tree
  • grid
  • dialog

You could also use one of the more complex roles like:

  • treegrid
  • alertdialog

There are two additional roles that are slightly more contentious but may do just fine.

  • tooltip
  • status

To understand why tooltip and status could be valid popover roles, we need to take a detour into the world of tooltips.

A Note on Tooltips

From a visual perspective, a tooltip is a popover because it contains a tiny window that pops up when the tooltip is displayed.

I included tooltip in the mental model because it is reasonable to implement tooltip with the Popover API.

<div popver role="tooltip">...</div>

The tooltip role doesn’t do much in screen readers today so you need to use aria-describedby to create accessible tooltips. But it is still important because it may extend accessibility support for some software.

But, from an accessibility standpoint, tooltips are not popovers. In the accessibility world, tooltips must not contain interactive content. If they contain interactive content, you’re not looking at a tooltip, but a dialog.

You’re thinking of dialogs. Use a dialog.

Heydon Pickering, “Your Tooltips are Bogus”

This is also why aria-haspopup doesn’t include tooltiparia-haspopup is supposed to signify interactive content but a tooltip must not contain interactive content.

With that, let’s move on to status which is an interesting role that requires some explanation.

Why status?

Tooltips have a pretty complex history in the world of accessible interfaces so there’s a lot of discussion and contention over it.

To keep things short (again), there’s an accessibility issue with tooltips since tooltips should only show on hover. This means screen readers and mobile phone users won’t be able to see those tooltips (since they can’t hover on the interface).

Steve Faulkner created an alternative — toggletips — to fill the gap. In doing so, he explained that toggletip content must be announced by screen readers through live regions.

When initially displayed content is announced by (most) screen readers that support aria-live

Heydon Pickering later added that status can be used in his article on toggletips.

We can supply an empty live region, and populate it with the toggletip “bubble” when it is invoked. This will both make the bubble appear visually and cause the live region to announce the tooltip’s information.

<!-- Code example by Heydon -->
<span class="tooltip-container"> 
  <button type="button" aria-label="more info" data-toggletip-content="This clarifies whatever needs clarifying">i</button> 
  <span role="status"> 
    <span class="toggletip-bubble">This clarifies whatever needs clarifying</span> 
  </span>
</span>

This is why status can be a potential role for a popover, but you must use discretion when creating it.

That said, I’ve chosen not to include the status role in the Popover mental model because status is a live region role and hence different from the rest.

In Summary

Here’s a quick summary of the mental model:

  • Popover is an umbrella term for any kind of on-demand popup.
  • Dialog is one type of popover — a kind that creates a new window (or card) to contain some content.

When you internalize this, it’s not hard to see why the Popover API can be used with the dialog element.

<!-- Uses the popover API. Role needs to be determined manually -->
<div popover>...</div>

<!-- Dialog with the popover API. Role is dialog -->
<dialog popover>...</dialog>

<!-- Dialog that doesn't use the popover API. Role is dialog -->
<dialog>...</dialog>

When choosing a role for your popover, you can use one of these roles safely.

  • menu
  • listbox
  • tree
  • grid
  • treegrid
  • dialog
  • alertdialog

The added benefit is most of these roles work together with aria-haspopup which gained decent support in screen readers last year.

Of course, there are a couple more you can use like status and tooltip, but you won’t be able to use them together with aria-haspopup.

Further Reading


Clarifying the Relationship Between Popovers and Dialogs originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

Categories: Designing, Others Tags:

Clamp it! VS Code extension

October 23rd, 2024 No comments

There’s a lot of math behind fluid typography. CSS does make the math a lot easier these days, but even if you’re comfortable with that, writing the full declaration can be verbose and tough to remember. I know I often have to look it back up, despite having written it maybe a hundred times.

Silvestar made a little VS Code helper to abstract all that. Type in the target values you’re aiming for and the helper expands it on the spot.

He says ChatGPT did the initial lifting before he refined it. I can get behind this sort of AI-flavored usage. Start with an idea, find a starting point, look deeper at it, and shape it into something incredibly useful for a small, single purpose.


Clamp it! VS Code extension originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

Categories: Designing, Others Tags:

Unleash the Power of Scroll-Driven Animations

October 21st, 2024 No comments

I’m utterly behind in learning about scroll-driven animations apart from the “reading progress bar” experiments all over CodePen. Well, I’m not exactly “green” on the topic; we’ve published a handful of articles on it including this neat-o one by Lee Meyer published the other week.

Our “oldest” article about the feature is by Bramus, dated back to July 2021. We were calling it “scroll-linked” animation back then. I specifically mention Bramus because there’s no one else working as hard as he is to discover practical use cases where scroll-driven animations shine while helping everyone understand the concept. He writes about it exhaustively on his personal blog in addition to writing the Chrome for Developers documentation on it.

But there’s also this free course he calls “Unleash the Power of Scroll-Driven Animations” published on YouTube as a series of 10 short videos. I decided it was high time to sit, watch, and learn from one of the best. These are my notes from it.


Introduction

  • A scroll-driven animation is an animation that responds to scrolling. There’s a direct link between scrolling progress and the animation’s progress.
  • Scroll-driven animations are different than scroll-triggered animations, which execute on scroll and run in their entirety. Scroll-driven animations pause, play, and run with the direction of the scroll. It sounds to me like scroll-triggered animations are a lot like the CSS version of the JavaScript intersection observer that fires and plays independently of scroll.
  • Why learn this? It’s super easy to take an existing CSS animation or a WAAPI animation and link it up to scrolling. The only “new” thing to learn is how to attach an animation to scrolling. Plus, hey, it’s the platform!
  • There are also performance perks. JavsScript libraries that establish scroll-driven animations typically respond to scroll events on the main thread, which is render-blocking… and JANK! We’re working with hardware-accelerated animations… and NO JANK. Yuriko Hirota has a case study on the performance of scroll-driven animations published on the Chrome blog.
  • Supported in Chrome 115+. Can use @supports (animation-timeline: scroll()). However, I recently saw Bramus publish an update saying we need to look for animation-range support as well.
@supports ((animation-timeline: scroll()) and (animation-range: 0% 100%)) {
  /* Scroll-Driven Animations related styles go here */
  /* This check excludes Firefox Nightly which only has a partial implementation at the moment of posting (mid-September 2024). */
}
  • Remember to use prefers-reduced-motion and be mindful of those who may not want them.

Core Concepts: scroll() and ScrollTimeline

Let’s take an existing CSS animation.

@keyframes grow-progress {
  from {
    transform: scaleX(0);
  }
  to {
    transform: scaleX(1);
  }
}

#progress {
  animation: grow-progress 2s linear forwards;
}

Translation: Start with no width and scale it to its full width. When applied, it takes two seconds to complete and moves with linear easing just in the forwards direction.

This just runs when the #progress element is rendered. Let’s attach it to scrolling.

  • animation-timeline: The timeline that controls the animation’s progress.
  • scroll(): Creates a new scroll timeline set up to track the nearest ancestor scroller in the block direction.
#progress {
  animation: grow-progress 2s linear forwards;
  animation-timeline: scroll();
}

That’s it! We’re linked up. Now we can remove the animation-duration value from the mix (or set it to auto):

#progress {
  animation: grow-progress linear forwards;
  animation-timeline: scroll();
}

Note that we’re unable to plop the animation-timeline property on the animation shorthand, at least for now. Bramus calls it a “reset-only sub-property of the shorthand” which is a new term to me. Its value gets reset when you use the shorthand the same way background-color is reset by background. That means the best practice is to declare animation-timeline after animation.

/* YEP! */
#progress {
  animation: grow-progress linear forwards;
  animation-timeline: scroll();
}

/* NOPE! */
#progress {
  animation-timeline: scroll();
  animation: grow-progress linear forwards;
}

Let’s talk about the scroll() function. It creates an anonymous scroll timeline that “walks up” the ancestor tree from the target element to the nearest ancestor scroll. In this example, the nearest ancestor scroll is the :root element, which is tracked in the block direction.

We can name scroll timelines, but that’s in another video. For now, know that we can adjust which axis to track and which scroller to target in the scroll() function.

animation-timeline: scroll(<axis> <scroller>);
  • : The axis — be it block (default), inline, y, or x.
  • : The scroll container element that defines the scroll position that influences the timeline’s progress, which can be nearest (default), root (the document), or self.

If the root element does not have an overflow, then the animation becomes inactive. WAAPI gives us a way to establish scroll timelines in JavaScript with ScrollTimeline.

const $progressbar = document.querySelector(#progress);

$progressbar.style.transformOrigin = '0% 50%';
$progressbar.animate(
  {
    transform: ['scaleX(0)', 'scaleY()'],
  },
  {
    fill: 'forwards',
    timeline: new ScrollTimeline({
      source: document.documentElement, // root element
      // can control `axis` here as well
    }),
  }
)

Core Concepts: view() and ViewTimeline

First, we oughta distinguish a scroll container from a scroll port. Overflow can be visible or clipped. Clipped could be scrolling.

Diagram showing scrollport, scroll container, and scrollable overflow.

Those two bordered boxes show how easy it is to conflate scrollports and scroll containers. The scrollport is the visible part and coincides with the scroll container’s padding-box. When a scrollbar is present, that plus the scroll container is the root scroller, or the scroll container.

Diagram showing the root scroller.

A view timeline tracks the relative position of a subject within a scrollport. Now we’re getting into IntersectionObserver territory! So, for example, we can begin an animation on the scroll timeline when an element intersects with another, such as the target element intersecting the viewport, then it progresses with scrolling.

Bramus walks through an example of animating images in long-form content when they intersect with the viewport. First, a CSS animation to reveal an image from zero opacity to full opacity (with some added clipping).

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

.revealing-image {
  animation: reveal 1s linear both;
}

This currently runs on the document’s timeline. In the last video, we used scroll() to register a scroll timeline. Now, let’s use the view() function to register a view timeline instead. This way, we’re responding to when a .revealing-image element is in, well, view.

.revealing-image {
  animation: reveal 1s linear both;
  /* Rember to declare the timeline after the shorthand */
  animation-timeline: view();
}

At this point, however, the animation is nice but only completes when the element fully exits the viewport, meaning we don’t get to see the entire thing. There’s a recommended way to fix this that Bramus will cover in another video. For now, we’re speeding up the keyframes instead by completing the animation at the 50% mark.

@keyframes reveal {
  from {
    opacity: 0;
    clip-path: inset(45% 20% 45% 20%);
  }
  50% {
    opacity: 1;
    clip-path: inset(0% 0% 0% 0%);
  }
}

More on the view() function:

animation-timeline: view(<axis> <view-timeline-inset>);

We know from the scroll() function — it’s the same deal. The is a way of adjusting the visibility range of the view progress (what a mouthful!) that we can set to auto (default) or a . A positive inset moves in an outward adjustment while a negative value moves in an inward adjustment. And notice that there is no argument — a view timeline always tracks its subject’s nearest ancestor scroll container.

OK, moving on to adjusting things with ViewTimeline in JavaScript instead.

const $images = document.querySelectorAll(.revealing-image);

$images.forEach(($image) => {
  $image.animate(
    [
      { opacity: 0, clipPath: 'inset(45% 20% 45% 20%)', offset: 0 }
      { opacity: 1; clipPath: 'inset(0% 0% 0% 0%)', offset: 0.5 }
    ],
    {
      fill: 'both',
      timeline: new ViewTimeline({
        subject: $image,
        axis: 'block', // Do we have to do this if it's the default?
      }),
    }
  }
)

This has the same effect as the CSS-only approach with animation-timeline.

Timeline Ranges Demystified

Last time, we adjusted where the image’s reveal animation ends by tweaking the keyframes to end at 50% rather than 100%. We could have played with the inset(). But there is an easier way: adjust the animation attachment range,

Most scroll animations go from zero scroll to 100% scroll. The animation-range property adjusts that:

animation-range: normal normal;

Those two values: the start scroll and end scroll, default:

animation-range: 0% 100%;

Other length units, of course:

animation-range: 100px 80vh;

The example we’re looking at is a “full-height cover card to fixed header”. Mouthful! But it’s neat, going from an immersive full-page header to a thin, fixed header while scrolling down the page.

@keyframes sticky-header {
  from {
    background-position: 50% 0;
    height: 100vh;
    font-size: calc(4vw + 1em);
  }
  to {
    background-position: 50% 100%;
    height: 10vh;
    font-size: calc(4vw + 1em);
    background-color: #0b1584;
  }
}

If we run the animation during scroll, it takes the full animation range, 0%-100%.

.sticky-header {
  position: fixed;
  top: 0;

  animation: sticky-header linear forwards;
  animation-timeline: scroll();
}

Like the revealing images from the last video, we want the animation range a little narrower to prevent the header from animating out of view. Last time, we adjusted the keyframes. This time, we’re going with the property approach:

.sticky-header {
  position: fixed;
  top: 0;

  animation: sticky-header linear forwards;
  animation-timeline: scroll();
  animation-range: 0vh 90vh;
}

We had to subtract the full height (100vh) from the header’s eventual height (10vh) to get that 90vh value. I can’t believe this is happening in CSS and not JavaScript! Bramus sagely notes that font-size animation happens on the main thread — it is not hardware-accelerated — and the entire scroll-driven animation runs on the main as a result. Other properties cause this as well, notably custom properties.

Back to the animation range. It can be diagrammed like this:

Visual demo showing the animation's full range.
The animation “cover range”. The dashed area represents the height of the animated target element.

Notice that there are four points in there. We’ve only been chatting about the “start edge” and “end edge” up to this point, but the range covers a larger area in view timelines. So, this:

animation-range: 0% 100%; /* same as 'normal normal' */

…to this:

animation-range: cover 0% cover 100%; /* 'cover normal cover normal' */

…which is really this:

animation-range: cover;

So, yeah. That revealing image animation from the last video? We could have done this, rather than fuss with the keyframes or insets:

animation-range: cover 0% cover 50%;

So nice. The demo visualization is hosted at scroll-driven-animations.style. Oh, and we have keyword values available: contain, entry, exit, entry-crossing, and exit-crossing.

Showing a contained animation range.
contain
Showing an entry animation range.
entry
Showing an exit animation range.
exit

The examples so far are based on the scroller being the root element. What about ranges that are taller than the scrollport subject? The ranges become slightly different.

An element larger than the scrollport where contain equals 100% when out of range but 0% before it actually reaches the end of the animation.
Just have to be aware of the element’s size and how it impacts the scrollport.

This is where the entry-crossing and entry-exit values come into play. This is a little mind-bendy at first, but I’m sure it’ll get easier with use. It’s clear things can get complex really quickly… which is especially true when we start working with multiple scroll-driven animation with their own animation ranges. Yes, that’s all possible. It’s all good as long as the ranges don’t overlap. Bramus uses a contact list demo where contact items animate when they enter and exit the scrollport.

@keyframes animate-in {
  0% { opacity: 0; transform: translateY: 100%; }
  100% { opacity: 1; transform: translateY: 0%; }
}
@keyframes animate-out {
  0% { opacity: 1; transform: translateY: 0%; }
  100% { opacity: 0; transform: translateY: 100%; }
}

.list-view li {
  animation: animate-in linear forwards,
             animate-out linear forwards;
  animation-timeline: view();
  animation-range: entry, exit; /* animation-in, animation-out */
}

Another way, using entry and exit keywords directly in the keyframes:

@keyframes animate-in {
  entry 0% { opacity: 0; transform: translateY: 100%; }
  entry 100% { opacity: 1; transform: translateY: 0%; }
}
@keyframes animate-out {
  exit 0% { opacity: 1; transform: translateY: 0%; }
  exit 100% { opacity: 0; transform: translateY: 100%; }
}

.list-view li {
  animation: animate-in linear forwards,
             animate-out linear forwards;
  animation-timeline: view();
}

Notice that animation-range is no longer needed since its values are declared in the keyframes. Wow.

OK, ranges in JavaScript.:

const timeline = new ViewTimeline({
  subjext: $li,
  axis: 'block',
})

// Animate in
$li.animate({
  opacity: [ 0, 1 ],
  transform: [ 'translateY(100%)', 'translateY(0)' ],
}, {
  fill: 'forwards',
  // One timeline instance with multiple ranges
  timeline,
  rangeStart: 'entry: 0%',
  rangeEnd: 'entry 100%',
})

Core Concepts: Timeline Lookup and Named Timelines

This time, we’re learning how to attach an animation to any scroll container on the page without needing to be an ancestor of that element. That’s all about named timelines.

But first, anonymous timelines track their nearest ancestor scroll container.

<html> <!-- scroll -->
  <body>
    <div class="wrapper">
      <div style="animation-timeline: scroll();"></div>
    </div>
  </body>
</html>

Some problems might happen like when overflow is hidden from a container:

<html> <!-- scroll -->
  <body>
    <div class="wrapper" style="overflow: hidden;"> <!-- scroll -->
      <div style="animation-timeline: scroll();"></div>
    </div>
  </body>
</html>

Hiding overflow means that the element’s content block is clipped to its padding box and does not provide any scrolling interface. However, the content must still be scrollable programmatically meaning this is still a scroll container. That’s an easy gotcha if there ever was one! The better route is to use overflow: clipped rather than hidden because that prevents the element from becoming a scroll container.

Hiding oveflow = scroll container. Clipping overflow = no scroll container. Bramus says he no longer sees any need to use overflow: hidden these days unless you explicitly need to set a scroll container. I might need to change my muscle memory to make that my go-to for hiding clipping overflow.

Another funky thing to watch for: absolute positioning on a scroll animation target in a relatively-positioned container. It will never match an outside scroll container that is scroll(inline-nearest) since it is absolute to its container like it’s unable to see out of it.

We don’t have to rely on the “nearest” scroll container or fuss with different overflow values. We can set which container to track with named timelines.

.gallery {
  position: relative;
}
.gallery__scrollcontainer {
  overflow-x: scroll;
  scroll-timeline-name: --gallery__scrollcontainer;
  scroll-timeline-axis: inline; /* container scrolls in the inline direction */
}
.gallery__progress {
  position: absolute;
  animation: progress linear forwards;
  animation-timeline: scroll(inline nearest);
}

We can shorten that up with the scroll-timeline shorthand:

.gallery {
  position: relative;
}
.gallery__scrollcontainer {
  overflow-x: scroll;
  scroll-timeline: --gallery__scrollcontainer inline;
}
.gallery__progress {
  position: absolute;
  animation: progress linear forwards;
  animation-timeline: scroll(inline nearest);
}

Note that block is the scroll-timeline-axis initial value. Also, note that the named timeline is a dashed-ident, so it looks like a CSS variable.

That’s named scroll timelines. The same is true of named view timlines.

.scroll-container {
  view-timeline-name: --card;
  view-timeline-axis: inline;
  view-timeline-inset: auto;
  /* view-timeline: --card inline auto */
}

Bramus showed a demo that recreates Apple’s old cover-flow pattern. It runs two animations, one for rotating images and one for setting an image’s z-index. We can attach both animations to the same view timeline. So, we go from tracking the nearest scroll container for each element in the scroll:

.covers li {
  view-timeline-name: --li-in-and-out-of-view;
  view-timeline-axis: inline;

  animation: adjust-z-index linear both;
  animation-timeline: view(inline);
}
.cards li > img {
   animation: rotate-cover linear both;
   animation-timeline: view(inline);
}

…and simply reference the same named timelines:

.covers li {
  view-timeline-name: --li-in-and-out-of-view;
  view-timeline-axis: inline;

  animation: adjust-z-index linear both;
  animation-timeline: --li-in-and-out-of-view;;
}
.cards li > img {
   animation: rotate-cover linear both;
   animation-timeline: --li-in-and-out-of-view;;
}

In this specific demo, the images rotate and scale but the updated sizing does not affect the view timeline: it stays the same size, respecting the original box size rather than flexing with the changes.

Phew, we have another tool for attaching animations to timelines that are not direct ancestors: timeline-scope.

timeline-scope: --example;

This goes on an parent element that is shared by both the animated target and the animated timeline. This way, we can still attach them even if they are not direct ancestors.

<div style="timeline-scope: --gallery">
  <div style="scroll-timeline: --gallery-inline;">
     ...
  </div>
  <div style="animation-timeline: --gallery;"></div>
</div>
Illustrating the relationship between a scroll target and container when they are not ancestors, but siblings.

It accepts multiple comma-separated values:

timeline-scope: --one, --two, --three;
/* or */
timeline-scope: all; /* Chrome 116+ */

There’s no Safari or Firefox support for the all kewword just yet but we can watch for it at Caniuse (or the newer BCD Watch!).

This video is considered the last one in the series of “core concepts.” The next five are more focused on use cases and examples.

Add Scroll Shadows to a Scroll Container

In this example, we’re conditionally showing scroll shadows on a scroll container. Chris calls scroll shadows one his favorite CSS-Tricks of all time and we can nail them with scroll animations.

Here is the demo Chris put together a few years ago:

CodePen Embed Fallback

That relies on having a background with multiple CSS gradients that are pinned to the extremes with background-attachment: fixed on a single selector. Let’s modernize this, starting with a different approach using pseudos with sticky positioning:

.container::before,
.container::after {
  content: "";
  display: block;
  position: sticky;
  left: 0em; 
  right 0em;
  height: 0.75rem;

  &::before {
    top: 0;
    background: radial-gradient(...);
  }
  
  &::after {
    bottom: 0;
    background: radial-gradient(...);
  }
}

The shadows fade in and out with a CSS animation:

@keyframes reveal {
  0% { opacity: 0; }
  100% { opacity: 1; }
}

.container {
  overflow:-y auto;
  scroll-timeline: --scroll-timeline block; /* do we need `block`? */

  &::before,
  &::after {
    animation: reveal linear both;
    animation-timeline: --scroll-timeline;
  }
}

This example rocks a named timeline, but Bramus notes that an anonymous one would work here as well. Seems like anonymous timelines are somewhat fragile and named timelines are a good defensive strategy.

The next thing we need is to set the animation’s range so that each pseudo scrolls in where needed. Calculating the range from the top is fairly straightforward:

.container::before {
  animation-range: 1em 2em;
}

The bottom is a little tricker. It should start when there are 2em of scrolling and then only travel for 1em. We can simply reverse the animation and add a little calculation to set the range based on it’s bottom edge.

.container::after {
  animation-direction: reverse;
  animation-range: calc(100% - 2em) calc(100% - 1em);
}

Still one more thing. We only want the shadows to reveal when we’re in a scroll container. If, for example, the box is taller than the content, there is no scrolling, yet we get both shadows.

Shadows on the top and bottom edges of the content, but the content is shorter than the box height, resulting in the shadow being in the middle of the box.

This is where the conditional part comes in. We can detect whether an element is scrollable and react to it. Bramus is talking about an animation keyword that’s new to me: detect-scroll.

@keyframes detect-scroll {
  from,
  to {
     --can-scroll: ; /* value is a single space and acts as boolean */
  }
}

.container {
  animation: detect-scroll;
  animation-timeline: --scroll-timeline;
  animation-fill-mode: none;
}

Gonna have to wrap my head around this… but the general idea is that --can-scroll is a boolean value we can use to set visibility on the pseudos:

.content::before,
.content::after {
    --vis-if-can-scroll: var(--can-scroll) visible;
    --vis-if-cant-scroll: hidden;

  visibility: var(--vis-if-can-scroll, var(--vis-if-cant-scroll));
}

Bramus points to this CSS-Tricks article for more on the conditional toggle stuff.

Animate Elements in Different Directions

This should be fun! Let’s say we have a set of columns:

<div class="columns">
  <div class="column reverse">...</div>
  <div class="column">...</div>
  <div class="column reverse">...</div>
</div>

The goal is getting the two outer reverse columns to scroll in the opposite direction as the inner column scrolls in the other direction. Classic JavaScript territory!

The columns are set up in a grid container. The columns flex in the column direction.

/* run if the browser supports it */
@supports (animation-timeline: scroll()) {

  .column-reverse {
    transform: translateY(calc(-100% + 100vh));
    flex-direction: column-reverse; /* flows in reverse order */
  }

  .columns {
    overflow-y: clip; /* not a scroll container! */
  }

}
The bottom edge of the outer columns are aligned with the top edge of the viewport.

First, the outer columns are pushed all the way up so the bottom edges are aligned with the viewport’s top edge. Then, on scroll, the outer columns slide down until their top edges re aligned with the viewport’s bottom edge.

The CSS animation:

@keyframes adjust-position {
  from /* the top */ {
    transform: translateY(calc(-100% + 100vh));
  }
  to /* the bottom */ {
    transform: translateY(calc(100% - 100vh));
  }
}

.column-reverse {
  animation: adjust-position linear forwards;
  animation-timeline: scroll(root block); /* viewport in block direction */
}

The approach is similar in JavaScript:

const timeline = new ScrollTimeline({
  source: document.documentElement,
});

document.querySelectorAll(".column-reverse").forEach($column) => {
  $column.animate(
    {
      transform: [
        "translateY(calc(-100% + 100vh))",
        "translateY(calc(100% - 100vh))"
      ]
    },
    {
      fill: "both",
      timeline,
    }
  );
}

Animate 3D Models and More on Scroll

This one’s working with a custom element for a 3D model:

<model-viewer alt="Robot" src="robot.glb"></model-viewer>

First, the scroll-driven animation. We’re attaching an animation to the component but not defining the keyframes just yet.

@keyframes foo {

}

model-viewer {
  animation: foo linear both;
  animation-timeline: scroll(block root); /* root scroller in block direction */
}

There’s some JavaScript for the full rotation and orientation:

// Bramus made a little helper for handling the requested animation frames
import { trackProgress } from "https://esm.sh/@bramus/sda-utilities";

// Select the component
const $model = document.QuerySelector("model-viewer");
// Animation begins with the first iteration
const animation = $model.getAnimations()[0];

// Variable to get the animation's timing info
let progress = animation.effect.getComputedTiming().progress * 1;
// If when finished, $progress = 1
if (animation.playState === "finished") progress = 1;
progress = Math.max(0.0, Math.min(1.0, progress)).toFixed(2);

// Convert this to degrees
$model.orientation = `0deg 0deg $(progress * -360)deg`;

We’re using the effect to get the animation’s progress rather than the current timed spot. The current time value is always measured relative to the full range, so we need the effect to get the progress based on the applied animation.

Scroll Velocity Detection

The video description is helpful:

Bramus goes full experimental and uses Scroll-Driven Animations to detect the active scroll speed and the directionality of scroll. Detecting this allows you to style an element based on whether the user is scrolling (or not scrolling), the direction they are scrolling in, and the speed they are scrolling with … and this all using only CSS.

First off, this is a hack. What we’re looking at is expermental and not very performant. We want to detect the animations’s velocity and direction. We start with two custom properties.

@keyframes adjust-pos {
  from {
    --scroll-position: 0;
    --scroll-position-delayed: 0;
  }
  to {
    --scroll-position: 1;
    --scroll-position-delayed: 1;
  }
}

:root {
  animation: adjust-pos linear both;
  animation-timeline: scroll(root);
}

Let’s register those custom properties so we can interpolate the values:

@property --scroll-position {
  syntax: "<number>";
  inherits: true;
  initial-value: 0;
}

@property --scroll-position-delayed {
  syntax: "<number>";
  inherits: true;
  initial-value: 0;
}

As we scroll, those values change. If we add a little delay, then we can stagger things a bit:

:root {
  animation: adjust-pos linear both;
  animation-timeline: scroll(root);
}

body {
  transition: --scroll-position-delayed 0.15s linear;
}

The fact that we’re applying this to the body is part of the trick because it depends on the parent-child relationship between html and body. The parent element updates the values immediately while the child lags behind just a tad. The evaluate to the same value, but one is slower to start.

We can use the difference between the two values as they are staggered to get the velocity.

:root {
  animation: adjust-pos linear both;
  animation-timeline: scroll(root);
}

body {
  transition: --scroll-position-delayed 0.15s linear;
  --scroll-velocity: calc(
    var(--scroll-position) - var(--scroll-position-delayed)
  );
}

Clever! If --scroll-velocity is equal to 0, then we know that the user is not scrolling because the two values are in sync. A positive number indicates the scroll direction is down, while a negative number indicates scrolling up,.

Showing values for the scroll position, the delayed position, and the velocity when scrolling occurs.

There’s a little discrepancy when scrolling abruptly changes direction. We can fix this by tighening the transition delay of --scroll-position-delayed but then we’re increasing the velocity. We might need a multiplier to further correct that… that’s why this is a hack. But now we have a way to sniff the scrolling speed and direction!

Here’s the hack using math functions:

body {
  transition: --scroll-position-delayed 0.15s linear;
  --scroll-velocity: calc(
    var(--scroll-position) - var(--scroll-position-delayed)
  );
  --scroll-direction: sign(var(--scroll-velocity));
  --scroll-speed: abs(var(--scroll-velocity));
}

This is a little funny because I’m seeing that Chrome does not yet support sign() or abs(), at least at the time I’m watching this. Gotta enable chrome://flags. There’s a polyfill for the math brought to you by Ana Tudor right here on CSS-Tricks.

Showing values for the scroll position, the delayed position, the velocity, the scroll direction, and the scroll speed when scrolling occurs.

So, now we could theoretically do something like skew an element by a certain amount or give it a certain level of background color saturation depending on the scroll speed.

.box {
  transform: skew(calc(var(--scroll-velocity) * -25deg));
  transition: background 0.15s ease;
  background: hsl(
    calc(0deg + (145deg * var(--scroll-direction))) 50 % 50%
  );
}

We could do all this with style queries should we want to:

@container style(--scroll-direction: 0) { /* idle */
  .slider-item {
    background: crimson;
  }
}
@container style(--scroll-direction: 1) { /* scrolling down */
  .slider-item {
    background: forestgreen;
  }
}
@container style(--scroll-direction: -1) { /* scrolling down */
  .slider-item {
    background: lightskyblue;
  }
}

Custom properties, scroll-driven animations, and style queries — all in one demo! These are wild times for CSS, tell ya what.

Outro

The tenth and final video! Just a summary of the series, so no new notes here. But here’s a great demo to cap it off.

CodePen Embed Fallback

Unleash the Power of Scroll-Driven Animations originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

Categories: Designing, Others Tags:

3 Essential Design Trends, November 2024

October 21st, 2024 No comments

Touchable texture, distinct grids, and two-column designs are some of the most trending website design elements of November 2024.

Categories: Designing, Others Tags:

3 Essential Design Trends, November 2024

October 21st, 2024 No comments
Sales More uses motion with texture to create something that you can’t quite identify but that still has a reach-out-and-touch-it feel.

It’s hard to believe the end of the year is right around the corner. That isn’t stopping website designers from trying new things to keep things interesting as we begin to close out 2024, though.

With texture, grids, and two-column designs getting a lot of attention, this month’s website design trends include things you can try before the year ends.

Here’s what’s trending in design this month:

1. Touchable Textures

The term cozy often comes to mind when you think of cooler temperatures and fall or winter. Just the word can make you feel the texture of a great sweater or blanket.

Now take that concept and apply it visually. That’s the idea behind this design trend with a set of images that look like you can touch them. Just seeing these textures makes you understand how they might feel or interact in the real world.

Blurring the lines between digital spaces and reality has been a trending theme all year, and these designs take that concept to a truly visual level. (I could almost feel every one of these examples.)

The challenge with this design trend is you have to really plan it. You need the perfect visual element – sometimes still, sometimes animated. And it has to look so real that website visitors think they can touch it.

These effects can be from photography or videography, but are rooted in the tactile nature of the artwork and how it interacts with other elements on the screen. Each of these examples does this in a very different way.

Sales & More uses motion with texture to create something that you can’t quite identify but that still has a reach-out-and-touch-it feel. You can almost imagine the tiny balls moving in your hands or a slight breeze when they move away from the main & shape. A flat background makes this design more striking, with a complete monotone color scheme and plenty of interactivity.

Every Exquisite Font Factory
uses multiple textured items to highlight letterforms and create visual interest. Because of shadowing, a simple background, and unusual usage (textures for letters), you can “feel” each one easily. This design uses a lot of different texture elements with immense detail in the design.

Moyceram features ceramic artwork with images that have a liquid finish you can almost feel. Even the painted eyelashes and eyebrows on the faces seem light and touchable. But the most distinct element may be that each mast looks wet, so that the emotion of each piece shows through.

Moyceram features ceramic artwork with images that have a liquid finish you can almost feel.

2. Distinct Grids

There are few things as nice as an amazing grid for a website design. A grid keeps content organized, balanced, and visually pleasing.

While grids are practically everywhere, this trend incorporates seeing them more distinctly than just using a grid for design purposes.

The feel is quite structured, allowing large amounts of content to have its own place without feeling cluttered. These designs can take a lot of shapes and fit with almost any type of content. The biggest driver is your need to organize.

Love and Money Agency uses a simple square block image grid to showcase their work. Blocks mix and match photos and moving images. Some blocks link to work and also feature hover states; some do not. This grid is clean and easy with a sort of create-your-own-adventure feel thanks to varying content types.

Love and Money Agency

Haydaysss uses a grid that looks and feels like a calendar. This is a visual that most people understand with ease. What’s more interesting here is that is all you get on the homepage. The grid design is for content access and entry and well as navigation. Each block has a simple hover animation to help you understand that it is interactive, and there’s a motion that crosses the screen every few seconds as well to keep visitors interested.

Haydaysss

Alec Tear follows our theme of grid boxes with his portfolio site. The monotone look is rather stunning on its own, but it is even more interesting with full-color hover states that lead you into different pieces of work. With an almost brutalist feel, this design aesthetic helps counterbalance some of the elaborateness of the work therein. The mono palette also helps you focus on what’s important here—lettering, which is showcased on this portfolio page.

Alec Tear

3. Two Columns

Another take on using grids is the two-column format. It is important to note that two columns does not automatically mean two equal columns. (Asymmetry can be rather nice.)

Each example uses two columns to help organize content and provide easily viewable text sections for the website. Full-width text is generally overwhelming to read on desktop screens, making multiple columns a viable option. Two columns also provide a nice logic for responsive stacking on smaller screens.

Vaersagod uses two columns that are somewhat independent of each other. The right column features visual work and examples for the portfolio site, while the left column scrolls with text. The theme carries to subsequent pages as well, and you almost have to click and scroll around to get the full feel for how it works.

Vaersagod

Richard George uses a more traditional split-screen design with two panels – one for text elements and a second for visuals. There’s a reason this design pattern never goes out of style – it works beautifully and is easy for visitors to interact with.

Richard George

RTRFM uses a great grid overall – you’ll have to click through and scroll to see it completely – but the two-column cards are a beautiful example of how to use two columns and feel super trendy at the same time. With a brutalist feel and plenty of content as well as linkable elements, these cards are highly interactive and handle a lot of information well. Small animated effects add to the overall feel, and this design scheme is perfect for the site’s content.

RTRFM

Conclusion

The trend in this collection that strikes me the most is the use of texture. These almost touchable designs make you want to interact more and inspire me to find video, photography, or three-dimensional illustrations that website visitors think they can feel.

The other trends are similar in their use of grids and distinct patterns to help website content feel organized—something that can be refreshing during a hectic time of the year. These are easier trends to conceptualize and use quickly.

No matter what option you like best, have fun, and make sure to experiment with your designs!

Categories: Designing, Others Tags:

Combining forces, GSAP & Webflow!

October 18th, 2024 No comments

Change can certainly be scary whenever a beloved, independent software library becomes a part of a larger organization. I’m feeling a bit more excitement than concern this time around, though.

If you haven’t heard, GSAP (GreenSock Animation Platform) is teaming up with the visual website builder, Webflow. This mutually beneficial advancement not only brings GSAP’s powerful animation capabilities to Webflow’s graphical user interface but also provides the GSAP team the resources necessary to take development to the next level.

GSAP has been independent software for nearly 15 years (since the Flash and ActionScript days!) primarily supported by Club GSAP memberships, their paid tiers which offer even more tools and plugins to enhance GSAP further. GSAP is currently used on more than 12 million websites.

I chatted with Cassie Evans — GSAP’s Lead Bestower of Animation Superpowers and CSS-Tricks contributor — who confidently expressed that GSAP will remain available for the wider web.

It’s a big change, but we think it’s going to be a good one – more resources for the core library, more people maintaining the GSAP codebase, money for events and merch and community support, a VISUAL GUI in the pipeline.

The Webflow community has cause for celebration as well, as direct integration with GSAP has been a wishlist item for a while.

The webflow community is so lovely and creative and supportive and friendly too. It’s a good fit.

I’m so happy for Jack, Cassie, and Rodrigo, as well as super excited to see what happens next. If you don’t want to take my word for it, check out what Brody has to say about it.


Combining forces, GSAP & Webflow! originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

Categories: Designing, Others Tags:

Mastering theme.json: You might not need CSS

October 18th, 2024 No comments

I totally get the goal here: make CSS more modular and scalable in WordPress. Put all your global WordPress theme styles in a single file, including variations. JSON offers a nicely structured syntax that’s easily consumable by JavaScript, thereby allowing the sweet affordance of loading exactly what we want when we want it.

The problem, to me, is that writing “CSS” in a theme.json file is a complete mental model switcher-oo. Rather than selectors, we have a whole set of objects we have to know about just to select something. We have JSON properties that look and feel like CSS properties, only they have to be camelCased being JavaScript and all. And we’re configuring features in the middle of the styles, meaning we’ve lost a clear separation of concerns.

I’m playing devil’s advocate, of course. There’s a lot of upside to abstracting CSS with JSON for the very niche purpose of theming CMS templates and components. But after a decade of “CSS-in-JS is the Way” I’m less inclined to buy into it. CSS is the bee’s knees just the way it is and I’m OK relying on it solely, whether it’s in the required style.css file or some other plain ol’ CSS file I generate. But that also means I’m losing out on the WordPress features that require you to write styles in a theme.json file, like style variations that can be toggled directly in the WordPress admin.

Regardless of all that, I’m linking this up because Justin does bang-up work (no surprise, really) explaining and illustrating the ways of CSS-in-WordPress. We have a complete guide that Ganesh rocked a couple of years ago. You might check that to get familiar with some terminology, jump into a nerdy deep dive on how WordPress generates classes from JSON, or just use the reference tables as a cheat sheet.


Mastering theme.json: You might not need CSS originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

Categories: Designing, Others Tags:

Solving Background Overflow With Inherited Border Radii

October 17th, 2024 No comments

One of the interesting (but annoying) things about CSS is the background of children’s elements can bleed out of the border radius of the parent element. Here’s an example of a card with an inner element. If the inner element is given a background, it can bleed out of the card’s border.

CodePen Embed Fallback

The easiest way to resolve this problem is to add overflow: hidden to the card element. I’m sure that’s the go-to solution most of us reach for when this happens.

But doing this creates a new problem — content outside the card element gets clipped off — so you can’t use negative margins or position: absolute to shift the children’s content out of the card.

CodePen Embed Fallback

There is a slightly more tedious — but more effective — way to prevent a child’s background from bleeding out of the parent’s border-radius. And that is to add the same border-radius to the child element.

The easiest way to do this is allowing the child to inherit the parent’s border-radius:

.child {
  border-radius: inherit;
}
CodePen Embed Fallback

If the border-radius shorthand is too much, you can still inherit the radius for each of the four corners on a case-by-case basis:

.child {
  border-top-left-radius: inherit;
  border-top-right-radius: inherit;
  border-bottom-left-radius: inherit;
  border-bottom-right-radius: inherit;
}

Or, for those of you who’re willing to use logical properties, here’s the equivalent. (For an easier way to understand logical properties, replace top and left with start, and bottom and right with end.)

.child {
  border-start-start-radius: inherit;
  border-top-end-radius: inherit;
  border-end-start-radius: inherit;
  border-end-end-radius: inherit;
}

Can’t we just apply a background on the card?

If you have a background directly on the .card that contains the border-radius, you will achieve the same effect. So, why not?

CodePen Embed Fallback

Well, sometimes you can’t do that. One situation is when you have a .card that’s split into two, and only one part is colored in.

CodePen Embed Fallback

So, why should we do this?

Peace of mind is probably the best reason. At the very least, you know you won’t be creating problems down the road with the radius manipulation solution.

This pattern is going to be especially helpful when CSS Anchor Positioning gains full support. I expect that would become the norm popover positioning soon in about 1-2 years.

That said, for popovers, I personally prefer to move the popover content out of the document flow and into the element as a direct descendant. By doing this, I prevent overflow: hidden from cutting off any of my popovers when I use anchor positioning.


Solving Background Overflow With Inherited Border Radii originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

Categories: Designing, Others Tags:

Close, Exit, Cancel: How to End User Interactions Well

October 16th, 2024 No comments
Screenshots of “ending” controls and navigation from Google Cloud, Gov.uk, and New York Times

What’s in a word? Actions. In the realm of user interfaces, a word is construed as the telltale of a control’s action. Sometimes it points us in the correct direction, and sometimes it leads us astray. We talk a lot about semantics in front-end web development, but outside of code, semantics are at the heart of copywriting where each word we convey can mean different things to different people. Words, if done right, add clarity and direction.

As a web user, I’ve come across words in user interfaces that have misled me. And not necessarily by design, either. Some words are synonymous with others and their true meaning depends entirely on context. Some words are easy to mistake for an unintended meaning because they are packed with so much meaning. A word might belong to a fellowship of interchangeable words.

Although I’m quite riled up when I misread content on a page — upset at the lack of clarity more than anything — as a developer, I can’t say I’ve always chosen the best possible words or combination of words for all the user interfaces I’ve ever made. But experience, both as a user and a developer, has elevated my commonsense when it comes to some of the literary choices I make while coding.

This article covers the words I choose for endings, to help users move away, and move on, without any confusion from the current process they are at on the screen. I went down this rabbit hole because I often find that ending something can mean many things — whether it be canceling an action, quitting an application, closing an element, navigating back, exiting a chat interaction… You get the idea. There are many ways to say that something is done, complete, and ready to move on to something else. I want to add clarity to that.

Getting Canceled

If there’s a Hall of Fame for button labels, this is the Babe Ruth of them all. “Cancel” is a widely used word to indicate an action that ends something. Cancel is a sharp, tenacious action. The person wants to bail on some process that didn’t go the way they expected it to. Maybe the page reveals a form that the person didn’t realize would be so long, so they want to back off. It could be something you have no control over whatsoever, like that person realizing they do not have their credit card information handy during checkout and they have to come back another time.

Cancel can feel personal at times, right? Don’t like the shipping costs calculated at checkout? Cancel the payment. Don’t like the newsletter? Cancel The Subscription. But really, the person only wants to undo an incorrect action or decision leaving no trace of it behind in favor of a clean slate to try again… or not.

The only times I feel betrayed by the word cancel is when the process I’m trying to end continues anyway. That comes up most when submitting forms with incorrect information. I enter something inadvertently, hit a big red Cancel button, yet the information I’ve “saved” persists to the extent that I either need to contact customer support or start looking for alternatives.

That’s the bottom line: Use “cancel” as an opportunity to confirm. It’s the person telling you, “Hey, that’s not actually what I meant to do,” and you get to step in and be the hero to wipe the mistake clean and set things up for a second chance. We’re not technically “ending” anything but rather starting clean and picking things back up for a better go. Think about that the next time you find yourself needing a label that encourages the user to try again. You might even consider synonyms that are less closely associated with closed endings, such as reset or retry.

“Cancel Subscription” mock-up

Quitting or Exiting?

Quit window, quit tab, quit app — now we’re talking about finality. When we “quit” or “exit” something, we’re changing course. We’ve made progress in one direction and decide it’s time to chart a different path. If we’re thinking about it in terms of freeway traffic, you might say that “quitting” is akin to pulling over and killing the engine, and “exiting” is taking leaving the freeway for another road. There’s a difference, although the two terms are closely related.

As far as we’re concerned as developers, quit and exit are hard stop points in an application. It’s been put to rest. Nothing else beyond this should be possible except its rebirth when the service is restarted or reopened. So, if your page is capable of nuking the current experience and the user takes it, then quit is the better label to make that point. We’re quitting and have no plans to restart or re-engage. If you were to “quit” your job, it’s not like your employer is expecting you to report for duty on Monday… or any other day for that matter.

But here’s my general advice about the word quit: only use it if you have to. I see very few use cases where we actually want to offer someone a true way to quit something. It’s so effective at conveying finality in web interfaces that it shuts the door on any future actions. For instance, I find that cancel often works in its place. And, personally, I find that saying “cancel payment” is more widely applicable. It’s softer and less rigid in the sense that it leaves the possibility to resume a process down the road.

Quit is also a simple process. Just clear everything and be gone. But if quitting means the user might lose some valuable data or progress, then that’s something they have to be warned about. In that case, exit and save may be better guidance.

I consider Exit the gentler twin of Quit. I prefer Quit just for the ultimatum of it. I see Exit used less frequently in interfaces than I see Quit. In rare cases, I might see Exit used specifically because of its softer nature to Quit even though “quitting” is the correct semantic choice given that the user really wants to wipe things clean and the assurance that nothing is left behind. Sometimes a “tougher” term is more reassuring.

Exit, however, is an excellent choice for actions that represent the end of human-to-human interactions — things like Exit Group, Exit Chat, Exit Streaming, Exit Class. If this person is kindly saying goodbye to someone or something but open to future interactions, allow them to exit when they’re done. They’re not quitting anything and we aren’t shoving them out the door.

“Exit Class” mock-up

Going Back (and Forth)

Let’s talk about navigation. That’s the way we describe moving around the internet. We navigate from one place to another, to another, to another, and so on. It’s a journey of putting one digital foot in front of the other on the way to somewhere. That journey comes to an end when we get to our destination… or when we “quit” or “exit” the journey as we discussed above.

But the journey may take twists and turns. Not all movement is linear on the web. That’s why we often leave breadcrumbs in interfaces, right? It’s wayfinding on the web and provides people with a way to go “back” where they came from. Maybe that person forgot a step and needs to head back in order to move forward again.

In other words, back displaces people — laterally and hierarchically. Laterally, back (and its synonym, previous), backtracks across the same level in a process, for instance, between two sections of the same form, or two pages of the same document. Hierarchically, back — not to mention more explicit variants like “home” — is a level above that in the navigation hierarchy.

I like the explicit nature of saying something like “Home” when it comes to navigating someone “back” to a location or state. There’s no ambiguity there: hey, let’s go back home. Being explicit opens you up to more verbose labels but brevity isn’t always the goal. Even in iconography, adding more detail to a visual can help add clarity. The same is true with content in user interfaces. My favorite example is the classic “Back to Top” button on many pages that navigate you to the “top” of the page. We’re going “back to the top” which would not have been clear if we had used “Back” alone. Back where? That’s an important question — particularly when working with in-page anchors — and the answer may not be as obvious to others as it is to you. Communicating that level of hierarchy explicitly is a navigational feature.

While the “Back to Top” example I gave is a better illustration of lateral displacement than hierarchical displacement, I tend to avoid the label back with any sort of lateral navigation because moving laterally typically involves navigating between states more than navigating between pages. For example, the user may be navigating from a “logged in” state to a “logged out” state. In this case, I prefer being even more explicit — e.g., Save and Go Back, or Cancel and Go Home — than hierarchical navigation because we’re changing states on top of moving away from something.

Navigation mock-up

Closing Down

Close is yet another term you’ll find in the wild for conveying the “end” of something. It’s quite similar to Back in the sense that it serves dual purposes. It can be for navigation — close the current page and go back — or it can be for canceling an action — close the current page, and either discard or save all the data entered so far.

I prefer Close for neither of those cases. If we’re in the context of navigation, I like the clarity of the more explicit guidance we discussed above, e.g., Go Back, Previous, or Go Home. Giving someone an instruction to Close doesn’t say where that person is going to land once navigating away from the current page. And if we’re dealing with actions, Save and Close affirms the person that their data will be saved, rather than simply “closing” it out. If we were to simply say “cancel” instead, the insinuation is that the user is quitting the action and can expect to lose their work.

The one time I do feel that “Close” is the ideal label is working with pop-up dialogues and modals. Placing “Close” at the top-right (or the block-start, inline-end edge if we’re talking logical directions) corner is more than clear enough about what happens to the pop-up or modal when clicking it. We can afford to be a little less explicit with our semantics when someone’s focus is trapped in a specific context.

The End.

I’ve saved the best for last, right? There’s no better way to connote an ending than simply calling it the “end”. It works well when we pair it with what’s ending.

End Chat. End Stream. End Webinar.

You’re terminating an established connection, not with a process, but with a human. And this is not some abrupt termination like Quit or Cancel. It’s more of a proper goodbye. Consider it also a synonym to Exit because the person ending the interaction may simply be taking a break. They’re not necessarily quitting something for good. Let’s leave the light on the front patio for them to return later and pick things back up..


And speaking of end, we’ve reached the end of this article. That’s the tricky, but liberating, thing about content semantics — some words may technically be correct but still mislead site visitors. It’s not that we’re ever trying to give someone bad directions, but it can still happen because this is a world where there are many ways of saying the same thing. Our goal is to be unambiguous and the milestone is clarity. Settling on the right word or combination of words takes effort. Anyone who has struggled with naming things in code knows about this. It’s the same for naming things outside of code.

I did not make an attempt to cover each and every word or way to convey endings. The point is that our words matter and we have all the choice and freedom in the world to find the best fit. But maybe you’ve recently run into a situation where you needed to “end” something and communicate that in an interface. Did you rely on something definitive and permanent (e.g. quit) or did you find that softer language (e.g. exit) was the better direction? What other synonyms did you consider? I’d love to know!

End Article.


Close, Exit, Cancel: How to End User Interactions Well originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

Categories: Designing, Others Tags: