::scroll-button()

Daniel Schwarz on

Get affordable and hassle-free WordPress hosting plans with Cloudways — start your free trial today.

Experimental: Check browser support before using this in production.

The ::scroll-button() pseudo-element adds accessible scroll buttons to scroll containers, giving users an additional way to navigate overflow content. To create a scroll button, simply specify a scroll direction while using the pseudo-element, and then declare the button’s content using the content property.

.carousel::scroll-button(right) {
  content: "⮕";
}

When a user clicks on a scroll button, they’ll scroll roughly 85% of the scroll container’s scrollport (assuming that nothing interferes with it, such as scroll snapping). All four scroll buttons are optional, so if you only want to allow scrolling in certain directions (e.g., left and right for a carousel), that’s totally fine.

Worried about scroll snapping? Don’t be. Scroll buttons work with CSS’s scroll snap module and other scroll-based features. Alternatively, or in addition to scroll buttons, you can use ::scroll-marker() to jump to specific items in the scroller.

Plus, accessibility is baked right in. No special HTML needed.

Syntax

<scroll-container>::scroll-button(<scroll-direction>) {
  content: <scroll-button-content>;
}
  • <scroll-container>: The scroll button will be placed inside this element and control its scroll position whenever possible. If the element isn’t a scroll container, the button will show (if it has a valid content value), but be :disabled.
  • <scroll-direction>:
    • Physical directions: up, right, down, left
    • Logical directions: block-start, block-end, inline-start, inline-end
  • <scroll-button-content>: The scroll button’s rendered content. Must be a valid value of the content property (otherwise the button won’t show).

Basic usage

/* Physical directions */
.scroll-container::scroll-button(up) {
  content: "⬆︎";
}

.scroll-container::scroll-button(right) {
  content: "⮕";
}

.scroll-container::scroll-button(down) {
  content: "⬇︎";
}

.scroll-container::scroll-button(left) {
  content: "⬅";
}

/* Logical directions */
.scroll-container::scroll-button(block-start) {
  writing-mode: horizontal-tb;
  content: "⬆︎";
}

.scroll-container::scroll-button(block-end) {
  writing-mode: horizontal-tb;
  content: "⬇︎";
}

.scroll-container::scroll-button(inline-start) {
  writing-mode: horizontal-tb;
  content: "⬅";
}

.scroll-container::scroll-button(inline-end) {
  writing-mode: horizontal-tb;
  content: "⮕";
}

Although not supported by any browser at the time of writing, we also have the next and prev buttons. These scroll buttons basically scroll forwards and backwards, respectively, along whichever axis is most scrollable, so ::scroll-button(next) would resolve to either ::scroll-button(block-end) or ::scroll-button(inline-end) and ::scroll-button(prev) would resolve to either ::scroll-button(block-start) or ::scroll-button(inline-start). If the scrollable width and scrollable height are the same, ::scroll-button(next) would resolve to ::scroll-button(block-end) and ::scroll-button(prev) would resolve to ::scroll-button(block-start).

/* block-end or inline-end */
.scroll-container::scroll-button(next) {
  /* ... */
}

/* block-start or inline-start */
.scroll-container::scroll-button(prev) {
  /* ... */
}

To select all scroll buttons, use the universal selector (*):

/* Select all scroll buttons */
.scroll-container::scroll-button(*) {
  /* Button styles */
}

Accessibility

Accessibility is built right into these scroll buttons.

For example, you don’t need to specify alternative text for the content property because it’s done for you, although you can overwrite it. However, this might not work as you’d expect — to be specific, visual text labels don’t imply accessibility labels, so if overwriting the browser’s default accessibility label, you must specify alternative text in addition to the visual text label even if they’re the same:

.scroll-container::scroll-button(right) {
  content: "⮕"; /* Announces "Scroll right" */
}

.scroll-container::scroll-button(right) {
  content: "Scroll forwards"; /* Still announces "Scroll right" */
}

.scroll-container::scroll-button(right) {
  /* Where content: "Visual text" / "Alt text"; */
  content: "Scroll forwards" / "Scroll forwards"; /* Announces "Scroll forwards" */
  content: "⮕" / "Scroll forwards"; /* Also announces "Scroll forwards" */
}

In addition to this, once the user reaches the end of the scrollable axis, the relevant scroll button is automatically disabled, both visually and accessibly.

Styling

Which brings us to the visuals of it all. To style disabled (or enabled) scroll buttons, combine the ::scroll-button() pseudo-element selector with the :disabled (or :enabled) pseudo-class:

::scroll-button(*):enabled {
  /* Enabled scroll button styles */
}
::scroll-button(*):disabled {
  /* Disabled scroll button styles */
}

Actually, since scroll buttons are basically <button> elements, you might want to set the cursor property for :enabled scroll buttons:

::scroll-button(*):enabled {
  cursor: pointer;
}

And, again, to style all scroll buttons:

/* Universally */
::scroll-button(*) {
  /* Button styles */
}

/* Within a specific container */
.scroll-container::scroll-button(*) {
  /* Button styles */
}

Finally, you’ll most likely want to put scroll-behavior: smooth on the scroll container to enable smooth scrolling.

@media (prefers-reduced-motion) {
  .scroll-container {
    scroll-behavior: smooth;
  }
}

The HTML:

<ul class="carousel">
  <li></li>
  <li></li>
  <li></li>
  <li></li>
  <li></li>
</ul>

The CSS:

.carousel {

  /* The width */
  --carousel-width: 100vw;
  width: var(--carousel-width);

  /* The height is half the width */
  aspect-ratio: 1 / 0.5;

  /* Implies flex-direction: row */
  display: flex;

  /* Enable scroll snapping on the x-axis */
  scroll-snap-type: x;

  li {

    /* Give carousel items the same width */
    width: var(--carousel-width);

    /* Prevent flexbox from overwriting said width */
    flex-shrink: 0;

    /* Instead of letting scroll buttons scroll 85% of the scrollport, scroll snap between carousel items instead */
    scroll-snap-align: center;
  }

  /* Show only one carousel item */
  overflow: hidden;

  /* Turn the carousel into an anchor */
  anchor-name: --carousel;

  /* Enable smooth scrolling */
  scroll-behavior: smooth;

    /* Select all scroll buttons */
  &::scroll-button(*) {

    /* Anchor them to the carousel */
    position-anchor: --carousel;

    /* Align them vertically */
    position: fixed;
    align-self: anchor-center;

    &:disabled {
      /* Disabled styles */
    }
  }

  /* Select the left scroll button */
  &::scroll-button(left) {

    /* Create the button */
    content: "⬅︎";

    /* Anchor the left side of the button to the left side of the carousel (offset by 10px) */
    left: calc(anchor(left) + 10px);

  }

  /* You know what to do */
  &::scroll-button(right) {
    content: "⮕";
    right: calc(anchor(right) + 10px);
  }
}
  • The carousel is responsive (defined by --carousel-width).
  • overflow: hidden also removes the scrollbars (not a requirement).
  • Since we’re showing off whole carousel items, scroll-snap-align can have any value.
  • You can align/anchor the scroll buttons in whichever way you’d like.

Example: Vertical scroll snapping

Same thing but vertical, with different button positions, the scrollbar not hidden, and mandatory scroll snapping (since users can scroll to any point manually):

Browser support

The ::scroll-button pseudo-element is only supported in Chrome 135+ and Edge 135+ at the time of writing. We can detect browser support for it if needed:

@supports selector(::scroll-button(*)) {
  /* ::scroll-button() supported */
}

@supports not selector(::scroll-button(*)) {
  /* ::scroll-button() not supported */
}

The same thing in JavaScript:

if (CSS.supports("selector(::scroll-button(*))")) {
  /* ::scroll-button() supported */
}

if (!CSS.supports("selector(::scroll-button(*))")) {
  /* ::scroll-button() not supported */
}

Specification

The ::scroll-button() pseudo-element is defined in the CSS Overflow Module Level 5 specification, which is currently in Working Draft status. This means that the information can change between now and the time when it becomes adopted as a formal Candidate Recommendation for browsers to implement.

More information