Zach’s ugly mug (his face) Zach Leatherman

<carou-scroll> Web Component

People standing near lighted carousel during night time
February 02, 2024

<carou-scroll> is a zero-dependency web component to add next and previous buttons to a scrollable container. I use it to share exported Keynote slides (as images) on my blog posts.

Some inspiration from the W3C WAI Carousel Tutorial (though this isn’t technically a carousel).

More real-world demos of Keynote slides:


  • Interaction compatible with scroll or touch.
  • No layout shift. Make sure you include the CSS snippet!
  • (Optional) Smooth scrolling with scroll-behavior: smooth.
  • (Optional) loop attribute to enable looping around from start/end.
  • (Optional) Next/Previous buttons can be placed anywhere in the document.
  • (Optional) <output> element can accessibly announce the current slide number (out of total number of slides).



You can install via npm or download the carouscroll.js JavaScript file manually.

npm install @zachleat/carouscroll --save

Add carouscroll.js to your site’s JavaScript assets.


The CSS here is crucial to reduce Layout Shift (CLS), set the aspect ratio of the slides, and to avoid loading loading="lazy" images on off-screen slides.

carou-scroll {
	display: flex;
	overflow-x: scroll;
	overflow-y: hidden;
carou-scroll > * {
	min-width: 100%;
	 /* Customize this as needed */
	aspect-ratio: 16/9;

<script type="module" src="carouscroll.js"></script>

<carou-scroll id="my-scroller">
	<img loading="lazy" src="" alt="">
	<img loading="lazy" src="" alt="">
	<!-- … -->

More examples

Add buttons (optional)

For maximum flexibility, these buttons can be placed anywhere in the document and are tied by an id back to the parent scroller.

Make sure you think about the before/after JavaScript experience here. This component will remove disabled for you but you can add additional styling via your own CSS: carou-scroll:defined {}.

<button type="button" disabled data-carousel-previous="my-scroller">Previous</button>
<button type="button" disabled data-carousel-next="my-scroller">Next</button>

Add output (optional)

This will update (and accessibly announce) a current status element with e.g. Slide 1 of 10 text.

For maximum flexibility, this element can be placed anywhere in the document and is tied by an id back to the parent scroller.

<output data-carousel-output="my-scroller"></output>

<!-- Or customize with your own markup -->
<output data-carousel-output="my-scroller" lang="ko">슬라이드 <span data-carousel-output-current>1</span>/<span data-carousel-output-total>10</span></output>

Make it loop around (optional)

Add the loop attribute.

<carou-scroll id="my-scroller" loop>

Smooth scrolling CSS (optional)

carou-scroll {
	scroll-behavior: smooth;

IndieWeb Avatar for image by Sally K

< Newer
Lessons learned moving Eleventy from CommonJS to ESM
Older >
The Good, The Bad, The Web Components

Zach Leatherman IndieWeb Avatar for a builder for the web and the creator/maintainer of IndieWeb Avatar for https://www.11ty.devEleventy (11ty), an award-winning open source site generator. At one point he became entirely too fixated on web fonts. He has given 81 talks in nine different countries at events like Beyond Tellerrand, Smashing Conference, Jamstack Conf, CSSConf, and The White House. Formerly part of CloudCannon, Netlify, Filament Group, NEJS CONF, and NebraskaJS. Learn more about Zach »


IndieWeb Avatar for https://www.alvinashcraft.comIndieWeb Avatar for https://mefody.devEdouard DuplessisAdrianJean Pierre KolbZach Leatherman :verify:BraveLilToasterBjörn BerkholzKhalid ⚡️Ryan MulliganThe Spicy WebDanny BlueChris BurnellLea RosemaRaymond CamdenHasan Aligapmiss


Edouard DuplessisJean Pierre KolbBraveLilToasterhaliphax ⛄lariAdam ArgyleJon DueckClay Ferris :ferris:Alexander ZeitlerRamón CorominasLindaKatCodesQuentin DelanceDanny BlueAaron In Iowanrk 9819Krzysztof JeziornysaiotbdonnellyNathan KnowlerChris BurnellLea RosemaChee Aun ????zeldadoudaStephen BellHasan Ali
  1. Maarten Stolte

    Maarten Stolte

    @mikestreety @DavidDarnes the lazy loading is not smart in a sense that it preloads I guess?

  2. Dave ???? :cursor_pointer:

    Dave ???? :cursor_pointer:

    @maarteuh @mikestreety sorry, not sure what you mean?

  3. Lea Rosema

    Lea Rosema

    @zachleat cool ????

  4. Zach Leatherman :verify:

    Zach Leatherman :verify:

    @lea yay! ????????

  5. Jared White

    Jared White

    @zachleat this is niiiiice

  6. Zach Leatherman :verify:

    Zach Leatherman :verify:

    @jaredwhite Thanks Jared!

  7. Zach Leatherman :verify:

    Zach Leatherman :verify:

    Thanks to @argyleink we’ve shipped v1.0.6 with `overscroll-behavior`: adds `overscroll-behavior` by argyleink · Pull Request #4 · zachleat/carouscroll

Shamelessly plug your related post

These are webmentions via the IndieWeb and

Sharing on social media?

This is what will show up when you share this post on Social Media:

How did you do this? I automated my Open Graph images. (Peer behind the curtain at the test page)