A carousel squeezes several items into the space of one. Script moves them through a fixed viewport, usually with buttons or dots and sometimes autoplay. Product galleries and homepage promotions use the pattern most.

Google’s web.dev carousel guide (2021) says slide content should arrive in the page’s HTML, not through script. Loading slides with JavaScript delays the first image and can hurt Largest Contentful Paint.

How do JavaScript carousels work?

See the Pen
Simple carousel with JS.
by luxonauta (@luxonauta)
on CodePen.

You will hear sliders, slideshows and rotators too, and they all mean the same widget. Designers file the lot under the carousel UI pattern.

The viewport is a fixed-size box with overflow hidden. Inside it sits the track, one wide row holding every slide side by side, and each slide is as wide as the viewport.

The track moves, not the slides. Felix Owino makes this point in his DEV Community vanilla JS walkthrough, where the motion belongs to the parent container while the other slides wait clipped on either side.

Most carousels move that track with a transform. The script sets translateX on the track and a CSS transition animates the shift.

A scroll-based version puts the slides in a scrollable row with scroll snap, and either the script or the user’s thumb changes the scroll offset.

The fade version doesn’t move anything. Slides stack in one spot, and the active one fades in while the rest fade out.

CSS handles the layout whichever way you go. The JavaScript layer covers what CSS can’t do alone, which is a current-index variable and event listeners for clicks and keys. Autoplay needs timers on top of that.

Every control a carousel offers, from dots to swipe, is just another way to change that index.

Native CSS, vanilla JavaScript or a library: which approach fits?

See the Pen
Modern CSS Carousels Collection
by Bogdan Sandu (@bogdansandu)
on CodePen.

Scroll snap covers a plain swipeable card row without much fuss. Once you want custom looping or autoplay, you’re in vanilla JavaScript territory, and the table below shows where each option gives out.

ApproachHow slides moveYou maintainBreaks when
Scroll snap with CSS buttons and markersNative browser scrollingA few CSS rulesYou need autoplay or looping
Scroll snap plus a short scriptScroll offset set by scriptA handful of JavaScript linesYou need looping or custom transitions
Vanilla JavaScripttranslateX on the trackState, controls, edge casesRapid clicks hit a running transition
LibraryThe library’s own engineA dependency and its updatesBundle weight outgrows a single gallery

Tuts+ builds a working slider in fewer than 15 lines of JavaScript by leaning on CSS smooth scrolling.

Chrome 135 generates scroll buttons that page the scroll area by 85%, and it places those buttons and the scroll markers in the tab order with their state tracked by the browser. Screen readers report the marker links like a tablist, with focusgroup-style keyboard behavior.

Chrome for Developers published the ::scroll-button() and ::scroll-marker() features on March 20, 2025. The Chrome team’s own Carousel Gallery site is built without JavaScript.

Support is the catch. MDN’s ::scroll-button() page, last updated in April 2026, still marks the feature experimental and outside Baseline.

The Chrome post listed Chrome 135 and Edge 135 as supported and Firefox and Safari as not, and it put cyclical scrolling on the future-work list. As of that post, native carousels do not loop.

Ship the native version as progressive enhancement, with a plain scrolling row as the fallback. Our walkthrough of a CSS carousel build covers the markup step by step.

A library earns its place when several carousels share one codebase, or when you need touch physics and looping out of the box. For one product gallery it’s a dependency you didn’t need, and honestly I’d skip it.

How to build a JavaScript carousel step by step

See the Pen
3D Carousel/Slider (CSS 3D & Vanilla JS)
by Jonathan Ching (@chingy)
on CodePen.

You write the markup and the CSS that puts the slides in a row, then add one function that moves the track to a given index.

  1. Write the markup with a labeled wrapper, one track and one element per slide. When several carousels share a page, give each its own IDs and point aria-labelledby at its own heading (LogRocket).
  2. Style the viewport with overflow hidden, give the track a flex row, and set flex: 0 0 100% on each slide. Add an aspect ratio so the height is reserved before images load.
  3. Store one state value, a currentIndex variable that starts at 0.
  4. Write goTo(index) to clamp the index and set translateX(-index * 100%) on the track. Put a transition on transform.
  5. Wire previous and next to call goTo with currentIndex minus or plus 1.
  6. Handle the ends by disabling the button at the edge, or by looping.
  7. Recalculate on resize, re-applying the transform whenever slide width or slides per view changes.

Animate transform, never left, top, width or marginTop. Layout-inducing properties cause layout shifts during slide transitions, per web.dev’s carousel guide by Katie Hempenius.

Reach for requestAnimationFrame only when the script drives every frame, as in a drag that follows the finger. For plain previous and next, a CSS transition is enough.

Looping without a visible jump

See the Pen
Infinity Carousel
by leusrox (@leusrox)
on CodePen.

Clone the first slide after the last and the last slide before the first. When a transition ends on a clone, switch the transition off and jump the track to the matching real slide.

Clicking fast during a running transition can still break the position. A public vanilla JavaScript practice project on GitHub (r0o0/carousel_vanillaJS) logs disabling both buttons while the animation runs.

Slides per view and responsive breakpoints

See the Pen
card-carousel
by Max Lykov (@frise)
on CodePen.

Slides per view is a number, not a layout.

Set each slide’s width to 100 divided by that number, in percent. Keep the number in a CSS custom property that changes inside media queries, and read it back in the script when you compute the maximum index.

Watch the last valid index. With 5 slides and 2 per view it’s 3, not 4, so clamp to slide count minus slides per view or the track slides into empty space.

How to add arrows, dots, swipe and keyboard controls

See the Pen
Simple Swiping Slider
by Katia (@katiasmet)
on CodePen.

Previous and next buttons, slide pickers, swipe and keyboard keys all do the same job, which is calling goTo with a new index.

Arrows should be native button elements labeled “Previous slide” and “Next slide”. Dots and thumbnails are buttons bound to a slide index, and the active one needs marking. Swipe runs on Pointer Events on the track, with a visible alternative next to it. Keyboard users get Tab to reach the controls and Enter or Space to activate them.

For swipe, listen for pointerdown, pointermove and pointerup on the track. Move the track with the finger, then commit to the next slide once the drag passes a distance threshold.

Set touch-action to pan-y on the track so vertical page scrolling keeps working.

WCAG 2.5.1 Pointer Gestures calls for a non-gesture alternative to swiping, and the arrow buttons already supply it (Sonja Weckenmann, Smashing Magazine).

The W3C ARIA Authoring Practices Guide describes a basic carousel with rotation and previous and next controls only. The tabbed style adds one tab stop for pickers built on the tabs pattern, and the grouped style adds one tab stop per picker button.

Grouped is the least friendly for keyboard users, because every dot adds another tab stop. Pick the tabbed style when the carousel has more than a few slides.

Activating the rotation, previous and next buttons does not move focus, so users can press them repeatedly without hunting for the control again (W3C APG).

web.dev adds two usability rules. Show the total slide count and the user’s position, and make the controls large and visible, since most carousels ship small, faint arrows.

How should carousel autoplay behave?

Skip autoplay if you can. If it has to run, it needs a pause control, and it should start paused for anyone who asked for reduced motion. Stopping on hover and focus is expected too.

WCAG 2.2 success criterion 2.2.2 (Level A) covers movement that starts automatically, lasts more than five seconds and sits beside other content. It requires a way to pause, stop or hide that movement, according to W3C’s Understanding document (updated August 2026).

Stopping on hover or focus helps, but it does not replace a pause control. W3C states that a pause lasting only while the user holds focus on the content is not a mechanism, because the page becomes unusable in the meantime.

The ARIA Authoring Practices Guide turns the requirement into concrete behavior. The rotation button comes first in the Tab order inside the carousel, and its label flips between “Stop slide rotation” and “Start slide rotation”.

Rotation stops when keyboard focus enters and stays stopped until the user restarts it. It also stops while the mouse hovers over the carousel. The slide wrapper carries aria-live=”off” while rotating and “polite” when it is not.

Read the prefers-reduced-motion media feature with matchMedia and start paused when it reports reduce. A user who set that preference in the operating system should never see slides move on their own.

web.dev is blunt about autoplay. It’s rarely a good choice, because motion pulls the eye away from more important content and users read moving banners as ads. WCAG 2.2.2 is more lenient and allows autoplay as long as the pause mechanism exists.

The two sources answer different questions. WCAG sets the minimum for conformance, while web.dev describes what works for readers.

No standard interval exists. web.dev ties display time to content: the more text on a slide, the longer it stays, because people read at different speeds.

What makes JavaScript carousels accessible?

The W3C WAI carousels tutorial boils accessibility down to 4 requirements: a way to pause motion, full keyboard operation, announced slide changes and sensible focus. Its guidance ties to 4 WCAG success criteria, all Level A: 1.3.1, 2.1.1, 2.2.2 and 4.1.2.

WCAG’s success criteria are the testable requirements. The APG carousel pattern and the WAI tutorial are guidance for meeting them, and the tutorial was last updated in April 2017, so check it against the current criterion text.

The stakes reach past the widget. Under WCAG’s non-interference requirement, every piece of content on a page must meet 2.2.2, so one rotating carousel can sink the whole page’s conformance.

That makes a carousel a page-level issue in any web accessibility audit, not a component-level one.

ARIA structure for the carousel and its slides

The APG builds the carousel from roles and role descriptions.

The container takes role region or group, with aria-roledescription set to carousel. Its label comes from aria-labelledby pointing at a visible heading, or from aria-label when no heading exists.

Each slide gets role group with aria-roledescription set to slide, plus its own label. That can be a heading, or a position such as “3 of 10” when slides have no unique names.

Neither label contains the word carousel or slide, because the role description already supplies it. Region is worth using only when the carousel deserves a landmark, and the APG leaves that call to the page’s information architecture.

The other two styles change the markup. In the tabbed style, slides become tabpanel elements without the slide role description, and the pickers become tabs in a tablist.

In the grouped style, the picker for the current slide gets aria-disabled instead of the disabled attribute. That keeps it in the tab sequence, which the APG says helps screen reader users.

ARIA names and describes, and it makes nothing operable. A div styled as an arrow that cannot take focus still fails 2.1.1, labels or not.

Hiding inactive slides badly is the other classic failure. The APG warns that slides moved off-screen instead of properly hidden leave screen reader users reading content they cannot see.

Keyboard access and focus order

Libraries disagree on where focus goes after a slide changes.

Slick’s accessibility option sets focus on the current slide after a change unless autoplay is on, and its docs tell you to enable focusOnChange as well for full accessibility compliance. That departs from the APG habit of leaving focus on the pressed control.

Both can satisfy 2.1.1. The W3C pattern is the safer default, because focus that jumps into slide content breaks repeated presses of Next.

Hide inactive slides with the hidden attribute or display none so their links leave the tab order. In scroll-snap carousels, where every slide stays in the DOM, use inert on the off-screen ones.

Keep a visible focus outline on every control, dots included.

How do JavaScript carousels affect Core Web Vitals?

web.dev’s carousel guide puts a well-built carousel at very little performance cost, so the damage comes from how the slides load. LCP and CLS are where it shows up. INP almost never does.

LCP goes first. A large above-the-fold carousel often holds the page’s LCP element. In a static carousel only the first slide is a candidate, while in an autoplay carousel any slide can end up as the final one, because candidates stop updating after the first user interaction.

Keep every slide in an autoplay carousel at the same intrinsic size. LCP uses the smaller of visible and intrinsic size, so mixed sizes let the LCP element change as slides rotate.

CLS is the sneakier one. Shifts from slide transitions in an autoplay carousel can count toward CLS and repeat indefinitely, and they are typically invisible to the eye. In a manual carousel they land in the short window after a user interaction and do not count.

Controls that move or get added on hover cause shifts too, so render them at load.

Carousels need little JavaScript, so INP rarely suffers. If a carousel script runs long tasks, web.dev says to replace the carousel tooling.

Two dated browser changes affect loading. Chrome 88 made images later removed from the DOM stay eligible as LCP candidates, which ended the LCP update that each slide swap used to trigger.

Chrome 121 gave horizontally scrolling images the same loading thresholds as vertical ones, so slides now load before they enter the viewport. The cost is more downloads, per web.dev.

So lazy loading defers less than it used to. Put the first slide in the HTML and never lazy-load it. Chrome’s LCP request discovery insight (October 2025) says to add fetchpriority=”high” or a preload, and to avoid loading=”lazy”. Later slides can be lazy-loaded, with progressive enhancement for controls on top of the static first slide.

Check what your library does here, because they differ. Swiper’s lazyPreload parameter handles loading=”lazy” images and preloads neighbors through lazyPreloadPrevNext, Slick’s lazyLoad accepts ondemand or progressive, and Splide ships its own lazy loading.

Fade carousels have a trap. Shopify’s theme performance docs say Chrome, Safari and Firefox skip a loading=”lazy” image hidden with display none but load one hidden with opacity 0, so stacked fade slides fetch everything anyway.

An Intersection Observer can hold off autoplay or slide loading until the carousel is on screen, and Splide’s Intersection extension does this for autoplay. Lighthouse has an audit for non-composited animations, which catches the slide transitions that cause the repeating CLS above.

Best JavaScript carousel libraries compared

Swiper makes sense if you want touch features and modules out of the box. If you’d rather start from a dependency-free core and extend it with plugins, Embla Carousel is the better fit, and Splide suits projects where built-in accessibility features and a small footprint come first.

Pick Slick or Owl Carousel only to maintain an old jQuery codebase. The order below follows fit for a new project, judged on dependencies, accessibility support, framework wrappers, license and maintenance signals.

LibraryBuild modelAccessibilityFrameworks
SwiperModular coreA11y module, opt-inReact, Vue, web component
Embla CarouselDependency-free, pluginsAccessibility pluginReact, Vue, Svelte, Solid
SplideDependency-freeLive region, play/pauseReact, Vue, Svelte
Glide.jsDependency-free, modularNone listedNone listed
SlickjQuery pluginTab and arrow keysNone listed

Swiper

Swiper is organized as a core plus modules that you register through the modules parameter, including Navigation, Pagination, Autoplay, A11y, Keyboard, Thumbs and Zoom. The docs (v14.3.0 in October 2026) list React, Vue and a web component named Swiper Element among their framework options.

On the plus side, a breakpoints parameter changes slides per view and spacing by screen width, and cssMode switches to CSS scroll snap for simple setups, with a reduced feature set. Methods such as slideTo and slideNext support custom controls too.

The downsides are real. Accessibility is a module you switch on, not a default. Loop mode rearranges slides and sets minimum slide counts, and breakpoints cannot change loop or effect.

Some roundups say Swiper follows the W3C carousel pattern out of the box. The API docs describe accessibility as the A11y module, turned on by a parameter, with labels such as prevSlideMessage set by you.

Embla Carousel

Embla is dependency-free and framework-agnostic under the MIT license, with plugins for Autoplay, Auto Scroll, Accessibility, Auto Height, Fade, Class Names and Wheel Gestures. The stable release on npm is 8.6.0, while version 9 is still a release candidate.

Setups are documented for React, Vue, Svelte and Solid, and there’s a third-party Angular wrapper.

One catch: the docs treat previous and next buttons as a guide to implement, not a component to import, so expect to write that markup.

Splide

Splide weighs 29 kB, or 12 kB gzipped, with no dependencies, according to its own site.

The site lists a live region, autoplay with play and pause buttons (you supply the extra HTML), breakpoints and a thumbnail slider. Extensions add Auto Scroll, Intersection, Video, Grid and URL Hash, and React, Vue and Svelte integrations exist.

One documented limit: the fade transition does not support multiple slides.

Glide.js

Glide.js is a dependency-free ES6 slider under the MIT license, and its modular build lets you drop unused modules to shrink the script.

Its homepage lists no accessibility features and no framework wrappers, so plan to add the pause control, labels and focus handling yourself.

Flickity

For open source projects, GPLv3 applies, and the project that uses Flickity has to carry GPLv3 too. Commercial sites, themes and apps are a different matter, and Metafizzy recommends a commercial license.

That license split is the deciding factor, because the library itself is solid. Its options page lists an accessibility option for keyboard navigation with the left and right arrow keys, it runs as a jQuery plugin or in vanilla JS, and npm shows 3.0.0 as the current release.

Bootstrap carousel

Bootstrap 5.3 builds its carousel from CSS 3D transforms and a little JavaScript, so a project that already ships Bootstrap gets one without another dependency.

Autoplay is opt-in with data-bs-ride, pauses on hover, and stops when the page is hidden through the Page Visibility API. Keyboard events and touch swipe are both on by default and can be switched off, and the animation depends on the prefers-reduced-motion media query.

There’s one gotcha. Without manual initialization, swipe listeners do not register until a user activates a control, except on data-bs-ride autoplaying carousels.

Bootstrap’s own docs recommend avoiding autoplay and adding your own pause control if you use it, and they list nested carousels as unsupported. Our Bootstrap carousel examples show the markup in practice.

Slick and Owl Carousel

See the Pen
Carousel Slider With Owl.js
by Envato Tuts+ (@tutsplus)
on CodePen.

Slick depends on jQuery (the README lists 1.7) and now carries a Version 2.0 note for jQuery 4. It turns its accessibility option on by default and pauses autoplay on hover and on focus.

Owl Carousel’s repository is marked deprecated, and its README calls the project dead and tells readers to switch.

Load order matters with Slick. If its script runs before jQuery, the page throws the familiar jQuery is not defined error.

Skip both on new projects, and keep Slick only where jQuery is already on the page.

When should you skip a JavaScript carousel?

Skip a carousel when its content has to be seen, such as a promotion or a main offer. Keep one for galleries, where users expect to move through related images of the same thing.

Nielsen Norman Group (January 2013) watched a user in the U.K. fail to find a £100 discount on the Siemens Appliances homepage. The offer sat in the biggest panel on the page, but the panel auto-rotated every 5 seconds.

NN/g’s case against auto-forwarding covers several kinds of user. Users with motor-skill difficulty cannot click before the panel moves, and low-literacy users often lack the time to read an item before it is removed. International users read more slowly in a non-native language and miss briefly shown panels.

Then there’s exposure. Each item is on screen only part of the time, which cuts its chance of being seen (the Siemens discount was visible 20% of the time). Losing control of the interface is plain annoying, and moving content looks like an advertisement, so users ignore it.

Erik Runyon’s numbers come from two time windows on the same site. His January 2013 post for the University of Notre Dame recorded about 1% of ND.edu visitors clicking a feature, with 84% of those clicks on position 1 (mid-October 2012 to January 22, 2013). His July 2013 update, covering January through June, put the click rate at 1.07% and position 1 at 89.1%.

Articles quote one figure or the other. Both are Runyon’s numbers for different periods.

The same update holds the awkward case for NN/g’s argument. The one auto-playing carousel Runyon tracked, on an aggregated news site, had 9.409% of visitors clicking a feature, well above the other four sites at roughly 1% to 3%.

Runyon says his team avoids autoplay anyway, and the audiences differ. The figure does not show that autoplay works, only that audience and content move click rates more than the rotation setting does.

Mobify’s e-commerce data, published in Smashing Magazine in 2015, shows the pattern succeeding elsewhere. Of the users who saw a mobile product image gallery carousel, 72% advanced it at least once, against the roughly 1% who clicked a homepage feature in Runyon’s data. The two measure different actions, which the Mobify authors acknowledge.

Read together, the sources split by purpose. A promotion asks users to leave the page, a gallery asks them to look at more of the same item, and the Mobify authors reach the same split: the answer for marketing banners might be an emphatic no, and for product galleries a definite yes.

Here are the numbers side by side.

  • 1.07% of ND.edu homepage visitors clicked a feature (Runyon, January to June 2013)
  • 89.1% of those clicks went to the first position (Runyon, same period)
  • 9.409% clicked a feature on the one auto-playing carousel Runyon tracked (June 2013 update)
  • 72% of users advanced a mobile product image gallery carousel at least once (Mobify, Smashing Magazine, 2015)

The pattern fails most often as a rotating hero image that carries the main offer, which is the Siemens setup. Put the call-to-action in static content, and let the carousel hold supporting material only.

web.dev adds a related rule: whatever the slides link to should stay reachable through other navigation paths, since most users never see every slide.

A single static panel with one message covers the same goal, and so does a visible grid where every item shows at once. A manual-only carousel with 3 or 4 slides and the strongest content first works as well.

Runyon suggests a maximum of four features, or better three, because click-throughs on later features fall sharply as the count grows.

JavaScript carousel examples

3D Carousel in Living Color!

See the Pen
CSS 3D transform Colorful Animated Carousel
by Edmundo Santos (@edmundojr)
on CodePen.

Snowboard Carousel with CSS Magic

See the Pen
A Dribble Design Implementation
by Sergiu Lucutar (@sergiulucutar)
on CodePen.

Momentum is the Name of the Game

See the Pen
Momentum carousel
by Elec (@elecweb)
on CodePen.

Scroll It, Drag It: Minimal Carousel

See the Pen
Minimal Carousel with horizontal scroll + snap, mobile & mouse friendly
by Frederic R. (@fredericrous)
on CodePen.

Infinite Carousel with a Twist

See the Pen
Slider with infinite loop with drag and scroll – horizontal
by Fabio Ottaviani (@supah)
on CodePen.

3D Carousel: It’s Showtime!

See the Pen
CSS 3D Carousel Room
by Daniel Velasquez (@Anemolo)
on CodePen.

Carousel with a Story to Tell

See the Pen
Vanilla JavaScript Carousel
by Aleksey Gromovich (@gromovich)
on CodePen.

Tom’s 3D Panorama Carousel

See the Pen
3D Carousel (v4 – panorama)
by Tom Miller (@creativeocean)
on CodePen.

Flexbox Carousel for Testimonials

See the Pen
Flexbox Testimonial Carousel
by James Pistell (@pistell)
on CodePen.

Planet-Loving Blob Carousel

See the Pen
Blob carousel for the planet
by Paulina Hetman (@pehaa)
on CodePen.

It’s Mario Time Carousel!

See the Pen
Custom Image Carousel
by Jarrod Thibodeau (@jarrodthibodeau)
on CodePen.

Stepping into 3D Shoe Wonderland

See the Pen
Shoe Swap 👟 | 3D CSS Product Transition Tutorial | @keyframers 2.9.0
by @keyframers (@keyframers)
on CodePen.

Carousel Dream Team

See the Pen
Carousel team
by Marco Barría (@fixcl)
on CodePen.

Looking Good with Perspective

See the Pen
Perspective carousel vanilla js
by Flowrome (@flowrome)
on CodePen.

Card Shuffle Carousel

See the Pen
Carousel
by Jesse (@WillyW)
on CodePen.

Retro Scroll, Anyone?

See the Pen
Marquee-like Content Scrolling
by Coding Journey (@Coding_Journey)
on CodePen.

Going Vertical with Tomislav

See the Pen
Mouse Driven Vertical Carousel
by Tomislav Jezidžić (@tjezidzic)
on CodePen.

Dive into 3D Swiping

See the Pen
3d Carousel Swipe
by alphardex (@alphardex)
on CodePen.

Synchronized Sliding Showcase

See the Pen
Portfolio Carousel with Synchronized Sliders
by lmgonzalves (@lmgonzalves)
on CodePen.

JavaScript Carousels FAQ

Is a carousel the same thing as a slider?

Yes. The W3C WAI carousels tutorial lists slideshows and sliders as other names for the same widget, and the ARIA Authoring Practices pattern is titled Carousel (Slide Show or Image Rotator).

Libraries blur it further: Splide describes itself as a slider/carousel.

How many slides should a carousel hold?

See the Pen
Gallery Viewer Carousel / Tiles
by Chris Rowlands (@Mrrowlie)
on CodePen.

Three or four, with the strongest slide first. Erik Runyon’s January 2013 Notre Dame data showed the most even click spread on the site with the fewest features, which had 3.

Mobify’s authors add that the first slide has to sell the next one, and that thumbnail controls get more slides viewed.

How do I use a carousel library in React, Vue or Angular?

Install the library’s framework package and use its components or hook. Swiper React ships inside the main swiper package, exports Swiper and SwiperSlide, and takes extra modules through a modules prop.

Embla’s embla-carousel-react returns a ref and an API from the useEmblaCarousel hook. Its Angular wrapper is third-party.

Do search engines index carousel slides?

Yes, when the slide content appears in the rendered HTML without a click or swipe. Google Search Central (last updated December 2025) says Google Search does not interact with the page, so native lazy loading and IntersectionObserver work, while loading that waits for a swipe is never seen.

Search Console’s URL Inspection Tool shows whether the image src attributes made it into the rendered HTML.

Testing a Carousel Before You Ship It

Test in a fixed order: keyboard, screen reader, then Lighthouse. Each pass catches failures the other two cannot see.

On the keyboard pass you Tab through every control with no mouse. The screen reader pass confirms slide labels and announcements, and the Lighthouse run confirms image loading, layout shifts and basic ARIA rules.

A keyboard failure forces a markup change, and that change alters what the screen reader announces and what Lighthouse measures. Fixing it last wastes the other 2 passes.

Lighthouse’s own documentation says only a subset of accessibility issues is detectable automatically, so a score of 100 never replaces the manual passes. The trade-off is time, because no CI job runs them for you.

Checked October 14, 2026: native CSS scroll markers and scroll buttons ship only in Chrome and Edge 135 and later, and this advice changes once Firefox and Safari both support them.

Bogdan Sandu
Latest posts by Bogdan Sandu (see all)