Carousels are an often used design pattern on the web. They are used in a variety of contexts, from product listing pages to slideshow like content. OpenUI has explored a range of carousel designs, showing that the specific layout and appearance can vary dramatically. They are also provided by many frameworks as components, however implementing a carousel correctly is complicated and often results in inconsistent and sometimes inaccessible implementations.
There are a variety of problems being solved by carousels, which we believe could be provided by a set of CSS features. Developers could then combine these CSS features to create the various designs. CSS-only component libraries could be built to further simplify this process.
Many carousels have markers or thumbnails which provide convenient navigation and a sense of overall progress through the carousel.
For individual items, an author can do this manually, though it requires writing extra elements which need to be kept up to date with the items to which they scroll. Script also needs to be used to get the desired scrolling behavior.
For dynamically content-sized pages, this can only currently be done with script which generates DOM. By having a way to automatically generate markers, many more advanced UI patterns can be solved in CSS.
Scroll markers require the combination of several behaviors:
Many carousels provide buttons for direct navigation to the next / previous item or page of content.
While they could create these <button>
elements as part of the page,
they are not semantically part of the list of items.
Further, the code for these adds a bit of additional complexity in terms of focus order, and tracking enabled state based on scroll position.
It is common and recommended practice (See use of aria-hidden) that only content on the active page is included in focus order. Users must use the carousel navigation controls (e.g. buttons / markers / swipe) to access content on future pages.
This is typically accomplished through the use of Javascript, as can be seen in the Web Accessibility Initiative Carousel example. However, this should be simple to express and declare in CSS.
Carousels often show lists of many items as a single group per page. Typically the user advances through one group at time. Many existing carousel libraries have complex logic to determine the number of items that fit per page and construct a per-page DOM.
However, automatic UA pagination can be done today through the use of column fragmentation, (e.g.):
.paginate {
overflow-x: scroll;
columns: 1;
}
This automatically handles paginating the content, but developers need access to a few additional styles to make these participate in a carousel UX. Specifically, developers need to be able to:
Many of these aspects of carousel experiences are highly differentiated and customizable or are building blocks that are shared by different experiences. As such, the proposed solutions here identify the specific features that make it possible to pick and style the pieces a developer may want for their carousel-like experience.
Anchor links are already used today to provide some of the behaviors of scroll markers. We can extend anchor links to support the missing behaviors by adding a grouping attribute. E.g.
<style>
.container {
scroll-marker-contain: auto;
}
</style>
<ul class="container">
<li><a href="#intro">Intro</a></li>
<li><a href="#overview">Overview</a></li>
<li><a href="#summary">Summary</a></li>
<li><a href="#faq">FAQ</a></li>
</ul>
By declaring that all of the anchor links in the list are part of a contained group,
the UA can ensure that one of them is determined to be the current marker,
allowing it to be styled with ::target-current
.
a::target-current {
font-weight: bold;
}
Roving tab-index selection can be added by adding the focusgroup attribute, combined with the guaranteed tab stop with the last-focused element automatically updated to the active marker.
<ul class="container" focusgroup>
<li><a tabindex="-1" href="#intro">Intro</a></li>
<li><a tabindex="-1" href="#overview">Overview</a></li>
<li><a tabindex="-1" href="#summary">Summary</a></li>
<li><a tabindex="-1" href="#faq">FAQ</a></li>
</ul>
Navigation controls are not part of the semantic content of the page. Sometimes, they may be a presentational choice for a semantic list of content. By allowing the creation of scroll marker groups from semantic lists, developers can automatically augment existing content without HTML modification.
E.g.
<style>
@media screen {
.slides {
overflow: auto;
counter-reset: slide-number;
scroll-marker-group: after;
}
.slide::scroll-marker {
counter-increment: slide-number;
content: counter(slide-number);
}
}
@media print {
.slides {
counter-reset: slide-number;
}
.slide::before {
counter-increment: slide-number;
content: "Slide " counter(slide-number);
}
}
</style>
<div class=slides>
<div class=slide></div>
<div class=slide></div>
<div class=slide></div>
<div class=slide></div>
<div class=slide></div>
<div class=slideshow>
This allows for the HTML structure to be focused on the content, with the controls to be generated automatically, optionally dependent on presentation media or screen real estate.
Similar to scroll markers, it should be easy to create scroll buttons.
If a developer has explicit <button>
elements, they could use the commandfor attribute targeting the scrolling container
with an action hint of scroll-<direction>
.
For use cases where the developer wishes to dynamically add scroll button controls,
the ::scroll-button(<direction>)
pseudo-element will establish a scroll button which
on activation scrolls the scrolling container of its owning element. E.g.
.scroller {
overflow: auto;
}
.scroller::scroll-button(down) {
content: "v";
}
.scroller::scroll-button(up) {
content: "^";
}
These pseudo-elements are focusable and behave as a button.
When activated, a scroll is performed in the indicated direction by a page (e.g. 85% of the scrollport similar to pressing the Page Down key on the keyboard).
When it is not possible to scroll in that direction, they are disabled and match the :disabled
pseudo-class.
The interactivity property #10711 combined with either a view timeline or snapped scroll state container query can be used to make the offscreen content inert. E.g.
Using a view timeline:
@keyframes interactive {
0% { interactivity: auto; }
100% { interactivity: auto; }
}
.slide {
/* Inert out of view items */
interactivity: inert;
/* Make interactive when in view */
animation: interactive;
animation-timeline: view(inline);
}
Using a scroll-state query:
.slide {
@container not scroll-state(snapped) {
interactivity: inert;
}
}
Column fragmentation allows for automatic pagination (example). Developers need access to a few additional styles to make the columns participate in a carousel UX.
The ::column
pseudo-element allows applying a limited set of styles to generated columns.
Specifically, this is limited to styles which do not affect the layout,
and thus can be applied post-layout.
E.g. the following example automatically paginates a list of items snapping each page into view.
ul {
overflow: auto;
container-type: size;
columns: 1;
}
ul::column {
scroll-snap-align: center;
}
This pseudo-element can additionally be used for the creation of ::scroll-marker
pseudo-elements:
ul::column::scroll-marker {
/* Marker styling */
}
With the features described, developers can create an automatically paginated carousel UX from a semantic list of items, e.g.
<ul class=carousel>
<li>Item 1</li>
<li>Item 2</li>
...
<li>Item n - 1</li>
<li>Item n</li>
</ul>
With the following CSS, the above HTML will be automatically paginated with a scroll marker per page with the active page highlighted, scroll buttons to go to the next / previous page, snapping to pages, and only having the user tab through content on the current page.
.carousel {
/* Automatically paginate into scrollport-sized columns. */
columns: 1;
/* Enable scrolling and set scrolling behaviors. */
overflow-x: auto;
overscroll-behavior-x: contain;
scroll-snap-type: x mandatory;
scroll-behavior: smooth;
/* Scroll marker creation and styling. */
scroll-marker-group: after;
&::scroll-marker-group {
height: 40px;
overflow-x: auto;
}
&::column::scroll-marker {
content: ' ';
&:target-current {
background: SelectedItem content-box;
}
&:focus {
border-color: Highlight;
}
}
/* Scroll button creation and styling. */
&::scroll-button(*) {
position: absolute;
}
&::scroll-button(left) {
content: '<';
left: 0;
}
&::scroll-button(right) {
content: '>';
right: 0;
}
/* Snap to columns as well. */
&::column {
scroll-snap-align: center;
}
& li {
/* Inert out of view items */
interactivity: inert;
/* Make interactive when in view */
animation: interactive;
animation-timeline: view(inline);
}
}
@keyframes interactive {
0% { interactivity: auto; }
100% { interactivity: auto; }
}
Of note, is that they can mix and match the features they want and style each piece.
The scroll marker feature provides a great drop-in way for a table of contents to automatically highlight the current section, e.g.
<style>
/* A floating table of contents similar to spec pages */
#table-of-contents {
position: fixed;
top: 0;
left: 0;
scroll-marker-contain: auto;
& a:target-current {
font-weight: bold;
}
}
</style>
<ul id=table-of-contents>
<li><a href="#s1">Section 1</a></li>
<li><a href="#s2">Section 2</a>
<ul>
<li><a href="#s2-1">Subsection 1</a>
<li><a href="#s2-2">Subsection 2</a>
<li>
</li>
...
<li><a href="#s2">Section n</a></li>
</ul>
A book reading interface can be created by using the automatic pagination. E.g.
<style>
.book {
columns: 1;
overflow: auto;
scroll-snap-type: x mandatory;
}
.book::column {
scroll-snap-align: center;
}
/* Optionally adding next/prev page buttons: */
.book::scroll-button(next) {
content: ">" / "Next page";
}
.book::scroll-button(prev) {
content: "<" / "Previous page";
}
There are a few carousel designs not currently addressed. This section enumerates and explores these areas.
Column fragmentation is a convenient way to automatically paginate a list of items horizontally,
however we should have a mechanism for paginating vertically as well.
Most likely, this will be of the form column-wrap: wrap
similar to the flex-wrap
property,
resulting in columns wrapping in the block direction after they overflow the inline direction.
With this a vertically paginated carousel could be defined as the following:
.carousel {
overflow: auto;
scroll-snap-type: y mandatory;
columns: 1;
column-wrap: wrap;
}
.carousel::column {
scroll-snap-align: center;
}
.carousel::column::scroll-marker {
/* marker styles */
}
Many paginated interfaces allow the user to peek into the subsequent / previous pages. Column layouts force an integer number of columns per page meaning they always perfectly fill the space. This could be supported by allowing for a fractional column value, or specifying the desired overlap region.
Many carousels allow scrolling from the last item in the list to the first, or vice versa. We expect that some form of cyclic overflow support (e.g. #5411) will make this trivial for authors to enable on top of these other features.
In the interim, authors could continue to use script, as they do today, to move content to the start / end of the carousel so that it can continuously scroll, or by overriding the next button behavior when at the last item to scroll back to the beginning.
Auto-advancing carousels introduces many potential accessibility issues if not implemented properly. The Web Accessibility Initiative Carousel Animations guidelines explores the necessary affordances. Most carousel experiences can be authored without automatically advancing sections, and in the mean-time author script could implement the animation following the WAI guidelines.
There are many other ways that we could deliver these capabilities.
An element could encapsulate a lot of the features without needing to express them directly as CSS properties. One of the main challenges with this approach is that carousels have a large amount of variation in their designs. This would likely add significant complexity to the design of a high level element, or require some of the individual features proposed anyways.
It would be nice for authors to be able to slot in rich content, as they would with a custom element. For example, they could provide a template of content to be created per page with a slot for the contents of that page.
One challenge is that the original content should retain its original structure. This may be possible by dynamically slotting elements to particular pages in a shadow tree.
It would be reasonable to think that if we had a way of flowing elements into another area, we could use that to create the group of scroll markers. E.g. you could imagine using the flow-into and flow-from properties as follows:
<style>
.markers {
flow-from: markers;
}
/* Generated within the element, similar to a ::before pseudo-element. */
li::scroll-marker {
flow-into: markers;
content: ' ';
}
</style>
<ul class=scroller>
<li>Item 1</li>
<li>Item 2</li>
<li>Item 3</li>
</ul>
<div class=markers>
</div>
This is in fact very similar to the original direction of this proposal, and is nice in its generality, but was abandoned for a few main reasons:
::scroll-marker-group
reduces the number of features needed to be combined to establish this.
A notable exception to this is that when not reflowed they could serve as self links #10498.::before
or ::after
pseudo-elements to contain flowing content,
but would likely introduce significant complexity. E.g.
.scroller::after {
flow-from: markers;
}
This is still a nice direction to be considered, and potentially which we could even explain the behavior of ::scroll-marker-group
in the future.
E.g. If we decide to do this later, we could explain that the default ::scroll-marker
flow-into
value is the flow-from
established by the ::scroll-marker-group
.
We could add a new built-in invoke-action
(see invokers) scrollTo
. When invoked, the invokeTarget
will be scrolled to within its ancestor scrolling container. E.g.
<button invoketarget="my-section" invokeaction="scrollTo">Scroll to section</button>
...
<section id="my-section">
This will be scrolled into view when you click the button
</section>
Invoker actions are only invoked on explicit activation, and interest actions are shown interest on focus or hover. For scroll markers, we want the action to be taken only when the selected marker changes, which occurs on focus navigation within the group, but not on hover.
As such, we'd propose adding the invoke
keyword to the focusgroup
attribute to allow invoking the invokeaction
on focusgroup focus changes. E.g.
<style>
#toc {
position: sticky;
top: 0;
}
</style>
<ul class="toc" focusgroup="invoke">
<li><button tabindex="-1" invoketarget="section-1" invokeaction="scrollTo">Section 1</button></li>
<li><button tabindex="-1" invoketarget="section-2" invokeaction="scrollTo">Section 2</button></li>
<li><button tabindex="-1" invoketarget="section-3" invokeaction="scrollTo">Section 3</button></li>
<li><button tabindex="-1" invoketarget="section-4" invokeaction="scrollTo">Section 4</button></li>
</ul>
<section id="section-1">...</section>
<section id="section-2">...</section>
<section id="section-3">...</section>
<section id="section-4">...</section>
Note that this example uses tabindex="-1" to apply the roving tab index with a guaranteed tab stop behavior from focusgroup.
This approach implements the navigation behavior, but notably does not explain how scroll markers would track the scroll state and allow styling the active marker.