|
| 1 | +# Carousel CSS features |
| 2 | + |
| 3 | +## Problem description |
| 4 | + |
| 5 | +Carousels are an often used design pattern on the web. |
| 6 | +They are used in a variety of contexts, |
| 7 | +from product listing pages to slideshow like content. |
| 8 | +OpenUI has [explored a range of carousel designs](https://open-ui.org/components/carousel.research/), |
| 9 | +showing that the specific layout and appearance can vary dramatically. |
| 10 | +They are also provided by many frameworks as components, |
| 11 | +however implementing a carousel correctly is complicated |
| 12 | +and often results in inconsistent and sometimes inaccessible implementations. |
| 13 | + |
| 14 | +There are a [variety of problems being solved by carousels](https://css.oddbird.net/overflow/explainer/), |
| 15 | +which we believe could be provided by a set of CSS features. |
| 16 | +Developers could then combine these CSS features to create the various designs. |
| 17 | +CSS-only component libraries could be built to further simplify this process. |
| 18 | + |
| 19 | +### Scroll markers |
| 20 | + |
| 21 | +Many carousels have markers or thumbnails |
| 22 | +which provide convenient navigation |
| 23 | +and a sense of overall progress through the carousel. |
| 24 | + |
| 25 | +For individual items, an author *can* do this manually, |
| 26 | +though it requires writing extra elements |
| 27 | +which need to be kept up to date with the items to which they scroll. |
| 28 | +Script also needs to be used to get the desired scrolling behavior. |
| 29 | + |
| 30 | +For dynamically content-sized pages, this can only currently be done with script which generates DOM. |
| 31 | +By having a way to automatically generate markers, |
| 32 | +many more advanced UI patterns can be solved in CSS. |
| 33 | + |
| 34 | +#### Requirements |
| 35 | + |
| 36 | +Scroll markers require the combination of several behaviors: |
| 37 | + |
| 38 | +1. They should scroll to the target on activation, |
| 39 | +2. only one of the scroll markers in a group should be active (and focusable) at a time (see [roving tab-index](https://developer.mozilla.org/en-US/docs/Web/Accessibility/Keyboard-navigable_JavaScript_widgets#technique_1_roving_tabindex)), |
| 40 | +3. arrow keys should cycle between the other markers, |
| 41 | +4. the correct one is automatically activated as a result of scrolling, and |
| 42 | +5. The active marker should be stylable. |
| 43 | + |
| 44 | +### Scroll buttons |
| 45 | + |
| 46 | +Many carousels provide buttons for direct navigation to the next / previous item or page of content. |
| 47 | +While they could create these `<button>` elements as part of the page, |
| 48 | +they are not semantically part of the list of items. |
| 49 | +Further, the code for these adds a bit of additional complexity in terms of focus order, and tracking enabled state based on scroll position. |
| 50 | + |
| 51 | +### CSS inert |
| 52 | + |
| 53 | +It is common and [recommended practice](https://www.w3.org/WAI/tutorials/carousels/working-example/) (See use of aria-hidden) that |
| 54 | +only content on the active page is included in focus order. |
| 55 | +Users must use the carousel navigation controls (e.g. buttons / markers / swipe) |
| 56 | +to access content on future pages. |
| 57 | + |
| 58 | +This is typically accomplished through the use of Javascript, |
| 59 | +as can be seen in the [Web Accessibility Initiative Carousel example](https://www.w3.org/WAI/tutorials/carousels/working-example/). |
| 60 | +However, this should be simple to express and declare in CSS. |
| 61 | + |
| 62 | +### Stylable columns |
| 63 | + |
| 64 | +Carousels often show lists of many items as a single group per page. |
| 65 | +Typically the user advances through one group at time. |
| 66 | +Many existing carousel libraries have complex logic |
| 67 | +to determine the number of items that fit per page and construct a per-page DOM. |
| 68 | + |
| 69 | +However, automatic UA pagination can be done today through the use of column fragmentation, ([e.g.](https://jsbin.com/modewaj/edit?html,output)): |
| 70 | +```css |
| 71 | +.paginate { |
| 72 | +overflow-x: scroll; |
| 73 | +columns: 1; |
| 74 | +} |
| 75 | +``` |
| 76 | + |
| 77 | +This automatically handles paginating the content, but developers need access to a few additional styles to make these participate in a carousel UX. |
| 78 | +Specifically, developers need to be able to: |
| 79 | +* Set scroll snap points on them. |
| 80 | +* Implicitly create scroll markers for each column. |
| 81 | + |
| 82 | +## Proposed solutions |
| 83 | + |
| 84 | +Many of these aspects of carousel experiences are highly differentiated and customizable |
| 85 | +or are building blocks that are shared by different experiences. |
| 86 | +As such, the proposed solutions here identify the specific features |
| 87 | +that make it possible to pick and style the pieces a developer may want for their carousel-like experience. |
| 88 | + |
| 89 | +### Scroll markers |
| 90 | + |
| 91 | +Anchor links are already used today to provide some of the behaviors of scroll markers. |
| 92 | +We can extend anchor links to support the missing behaviors |
| 93 | +by adding a grouping attribute. E.g. |
| 94 | + |
| 95 | +```html |
| 96 | +<style> |
| 97 | +.container { |
| 98 | + scroll-marker-contain: auto; |
| 99 | +} |
| 100 | +</style> |
| 101 | +<ul class="container"> |
| 102 | + <li><a href="#intro">Intro</a></li> |
| 103 | + <li><a href="#overview">Overview</a></li> |
| 104 | + <li><a href="#summary">Summary</a></li> |
| 105 | + <li><a href="#faq">FAQ</a></li> |
| 106 | +</ul> |
| 107 | +``` |
| 108 | + |
| 109 | +By declaring that all of the anchor links in the list are part of a contained group, |
| 110 | +the UA can ensure that one of them is determined to be the current marker, |
| 111 | +allowing it to be styled with `::target-current`. |
| 112 | + |
| 113 | +```css |
| 114 | +a::target-current { |
| 115 | + font-weight: bold; |
| 116 | +} |
| 117 | +``` |
| 118 | + |
| 119 | +Roving tab-index selection can be added by adding the focusgroup attribute, |
| 120 | +combined with the [guaranteed tab stop](https://open-ui.org/components/focusgroup.explainer/#guaranteed-tab-stop) |
| 121 | +with the [last-focused element](https://open-ui.org/components/focusgroup.explainer/#last-focused-memory) automatically updated to the active marker. |
| 122 | + |
| 123 | +```html |
| 124 | +<ul class="container" focusgroup> |
| 125 | + <li><a tabindex="-1" href="#intro">Intro</a></li> |
| 126 | + <li><a tabindex="-1" href="#overview">Overview</a></li> |
| 127 | + <li><a tabindex="-1" href="#summary">Summary</a></li> |
| 128 | + <li><a tabindex="-1" href="#faq">FAQ</a></li> |
| 129 | +</ul> |
| 130 | +``` |
| 131 | + |
| 132 | +#### ::scroll-marker-group and ::scroll-marker pseudo-elements |
| 133 | + |
| 134 | +Navigation controls are not part of the semantic content of the page. |
| 135 | +Sometimes, they may be a presentational choice for a semantic list of content. |
| 136 | +By allowing the creation of scroll marker groups from semantic lists, |
| 137 | +developers can automatically augment existing content without HTML modification. |
| 138 | + |
| 139 | +E.g. |
| 140 | +```html |
| 141 | +<style> |
| 142 | +@media screen { |
| 143 | + .slides { |
| 144 | + overflow: auto; |
| 145 | + counter-reset: slide-number; |
| 146 | + scroll-marker-group: after; |
| 147 | + } |
| 148 | + .slide::scroll-marker { |
| 149 | + counter-increment: slide-number; |
| 150 | + content: counter(slide-number); |
| 151 | + } |
| 152 | +} |
| 153 | +
|
| 154 | +@media print { |
| 155 | + .slides { |
| 156 | + counter-reset: slide-number; |
| 157 | + } |
| 158 | + .slide::before { |
| 159 | + counter-increment: slide-number; |
| 160 | + content: "Slide " counter(slide-number); |
| 161 | + } |
| 162 | +} |
| 163 | +</style> |
| 164 | +<div class=slides> |
| 165 | + <div class=slide></div> |
| 166 | + <div class=slide></div> |
| 167 | + <div class=slide></div> |
| 168 | + <div class=slide></div> |
| 169 | + <div class=slide></div> |
| 170 | +<div class=slideshow> |
| 171 | +``` |
| 172 | + |
| 173 | +This allows for the HTML structure to be focused on the content, |
| 174 | +with the controls to be generated automatically, |
| 175 | +optionally dependent on presentation media or screen real estate. |
| 176 | + |
| 177 | +### Scroll buttons |
| 178 | + |
| 179 | +Similar to scroll markers, it should be easy to create scroll buttons. |
| 180 | +If a developer has explicit `<button>` elements, they could use the [commandfor](https://open-ui.org/components/invokers.explainer/) attribute targeting the scrolling container |
| 181 | +with an [action hint](https://open-ui.org/components/invokers.explainer/) of `scroll-<direction>`. |
| 182 | + |
| 183 | +For use cases where the developer wishes to dynamically add scroll button controls, |
| 184 | +the `::scroll-button(<direction>)` pseudo-element will establish a scroll button which |
| 185 | +on activation scrolls the scrolling container of its owning element. E.g. |
| 186 | + |
| 187 | +```css |
| 188 | +.scroller { |
| 189 | + overflow: auto; |
| 190 | +} |
| 191 | + |
| 192 | +.scroller::scroll-button(down) { |
| 193 | + content: "v"; |
| 194 | +} |
| 195 | + |
| 196 | +.scroller::scroll-button(up) { |
| 197 | + content: "^"; |
| 198 | +} |
| 199 | +``` |
| 200 | + |
| 201 | +These pseudo-elements are focusable and behave as a button. |
| 202 | +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). |
| 203 | +When it is not possible to scroll in that direction, they are disabled and match the `:disabled` pseudo-class. |
| 204 | + |
| 205 | +### CSS inert |
| 206 | + |
| 207 | +The interactivity property [#10711](https://github.com/w3c/csswg-drafts/issues/10711) |
| 208 | +combined with either a view timeline or snapped scroll state container query |
| 209 | +can be used to make the offscreen content inert. E.g. |
| 210 | + |
| 211 | +Using a view timeline: |
| 212 | +```css |
| 213 | +@keyframes interactive { |
| 214 | + 0% { interactivity: auto; } |
| 215 | + 100% { interactivity: auto; } |
| 216 | +} |
| 217 | + |
| 218 | +.slide { |
| 219 | + /* Inert out of view items */ |
| 220 | + interactivity: inert; |
| 221 | + /* Make interactive when in view */ |
| 222 | + animation: interactive; |
| 223 | + animation-timeline: view(inline); |
| 224 | +} |
| 225 | +``` |
| 226 | + |
| 227 | +Using a scroll-state query: |
| 228 | +```css |
| 229 | +.slide { |
| 230 | + @container not scroll-state(snapped) { |
| 231 | + interactivity: inert; |
| 232 | + } |
| 233 | +} |
| 234 | +``` |
| 235 | + |
| 236 | +### Stylable columns |
| 237 | + |
| 238 | +Column fragmentation allows for automatic pagination ([example](https://jsbin.com/modewaj/edit?html,output)). |
| 239 | +Developers need access to a few additional styles to make the columns participate in a carousel UX. |
| 240 | + |
| 241 | +The `::column` pseudo-element allows applying a limited set of styles to generated columns. |
| 242 | +Specifically, this is limited to styles which do not affect the layout, |
| 243 | +and thus can be applied post-layout. |
| 244 | + |
| 245 | +E.g. the following [example](https://jsbin.com/defazup/edit?html,output) automatically paginates a list of items snapping each page into view. |
| 246 | +```css |
| 247 | +ul { |
| 248 | + overflow: auto; |
| 249 | + container-type: size; |
| 250 | + columns: 1; |
| 251 | +} |
| 252 | +ul::column { |
| 253 | + scroll-snap-align: center; |
| 254 | +} |
| 255 | +``` |
| 256 | + |
| 257 | +This pseudo-element can additionally be used for the creation of `::scroll-marker` pseudo-elements: |
| 258 | +```css |
| 259 | +ul::column::scroll-marker { |
| 260 | + /* Marker styling */ |
| 261 | +} |
| 262 | +``` |
| 263 | + |
| 264 | +## Putting it all together |
| 265 | + |
| 266 | +With the features described, developers can create an automatically paginated carousel UX from a semantic list of items, e.g. |
| 267 | + |
| 268 | +```html |
| 269 | +<ul class=carousel> |
| 270 | + <li>Item 1</li> |
| 271 | + <li>Item 2</li> |
| 272 | + ... |
| 273 | + <li>Item n - 1</li> |
| 274 | + <li>Item n</li> |
| 275 | +</ul> |
| 276 | +``` |
| 277 | + |
| 278 | +With the following CSS, |
| 279 | +the above HTML will be automatically paginated with |
| 280 | +a scroll marker per page with the active page highlighted, |
| 281 | +scroll buttons to go to the next / previous page, |
| 282 | +snapping to pages, |
| 283 | +and only having the user tab through content on the current page. |
| 284 | + |
| 285 | +```css |
| 286 | +.carousel { |
| 287 | + /* Automatically paginate into scrollport-sized columns. */ |
| 288 | + columns: 1; |
| 289 | + |
| 290 | + /* Enable scrolling and set scrolling behaviors. */ |
| 291 | + overflow-x: auto; |
| 292 | + overscroll-behavior-x: contain; |
| 293 | + scroll-snap-type: x mandatory; |
| 294 | + scroll-behavior: smooth; |
| 295 | + |
| 296 | + /* Scroll marker creation and styling. */ |
| 297 | + scroll-marker-group: after; |
| 298 | + &::scroll-marker-group { |
| 299 | + height: 40px; |
| 300 | + overflow-x: auto; |
| 301 | + } |
| 302 | + &::column::scroll-marker { |
| 303 | + content: ' '; |
| 304 | + &:target-current { |
| 305 | + background: SelectedItem content-box; |
| 306 | + } |
| 307 | + &:focus { |
| 308 | + border-color: Highlight; |
| 309 | + } |
| 310 | + } |
| 311 | + |
| 312 | + /* Scroll button creation and styling. */ |
| 313 | + &::scroll-button(*) { |
| 314 | + position: absolute; |
| 315 | + } |
| 316 | + &::scroll-button(left) { |
| 317 | + content: '<'; |
| 318 | + left: 0; |
| 319 | + } |
| 320 | + &::scroll-button(right) { |
| 321 | + content: '>'; |
| 322 | + right: 0; |
| 323 | + } |
| 324 | + |
| 325 | + /* Snap to columns as well. */ |
| 326 | + &::column { |
| 327 | + scroll-snap-align: center; |
| 328 | + } |
| 329 | + |
| 330 | + & li { |
| 331 | + /* Inert out of view items */ |
| 332 | + interactivity: inert; |
| 333 | + /* Make interactive when in view */ |
| 334 | + animation: interactive; |
| 335 | + animation-timeline: view(inline); |
| 336 | + } |
| 337 | +} |
| 338 | + |
| 339 | +@keyframes interactive { |
| 340 | + 0% { interactivity: auto; } |
| 341 | + 100% { interactivity: auto; } |
| 342 | +} |
| 343 | +``` |
| 344 | + |
| 345 | +## Future work |
| 346 | + |
| 347 | +There are a few carousel designs not currently addressed. |
| 348 | +This section enumerates and explores these areas. |
| 349 | + |
| 350 | +### Cyclic carousels |
| 351 | + |
| 352 | +Many carousels allow scrolling from the last item in the list to the first, or vice versa. |
| 353 | +We expect that some form of cyclic overflow support (e.g. [#5411](https://github.com/w3c/csswg-drafts/issues/5411)) |
| 354 | +will make this trivial for authors to enable on top of these other features. |
| 355 | + |
| 356 | +In the interim, authors could continue to use script, as they do today, |
| 357 | +to move content to the start / end of the carousel so that it can continuously scroll, |
| 358 | +or by overriding the next button behavior when at the last item to scroll back to the beginning. |
| 359 | + |
| 360 | +### Auto-advancing carousels |
| 361 | + |
| 362 | +Auto-advancing carousels introduces many potential accessibility issues if not implemented properly. |
| 363 | +The [Web Accessibility Initiative Carousel Animations](https://www.w3.org/WAI/tutorials/carousels/animations/) guidelines explores the necessary affordances. |
| 364 | +Most carousel experiences can be authored without automatically advancing sections, |
| 365 | +and in the mean-time author script could implement the animation following the WAI guidelines. |
0 commit comments