Slide one
Native scroll-snap carousel — no Embla, no dependencies.
Swipe, wheel or use the arrows. The dots track the position.
Scroll-snap carousel over a semantic section — no carousel library. Dots, arrows and keyboard paging wired by <mo-carousel>.
INSTALL
1pnpm dlx @sayagodev/mo add carouselOr import the files from the package (no copy):
1import "@sayagodev/mo/css/carousel.css";2import "@sayagodev/mo/js/carousel.js";
EXAMPLES
Native scroll-snap carousel — no Embla, no dependencies.
Swipe, wheel or use the arrows. The dots track the position.
Each slide snaps to the start edge of the track.
Slides are plain elements carrying data-carousel-slide.
Buttons disable at either end of the carousel.
Arrow keys page while the track is focused.
Slides can hold anything — cards, media, forms.
This one holds a .card article, like shadcn's demos.
The last slide. The next button is disabled here.
mo-carousel-change fires after each scroll settles.
1<mo-carousel>2 <section data-carousel aria-roledescription="carousel" aria-label="Highlights">3 <div data-carousel-track>4 <article class="card" data-carousel-slide>5 <header>6 <h3>Slide one</h3>7 <p>Native scroll-snap carousel — no Embla, no dependencies.</p>8 </header>9 <p>Swipe, wheel or use the arrows. The dots track the position.</p>10 </article>11 <article class="card" data-carousel-slide>12 <header>13 <h3>Slide two</h3>14 <p>Each slide snaps to the start edge of the track.</p>15 </header>16 <p>Slides are plain elements carrying data-carousel-slide.</p>17 </article>18 <article class="card" data-carousel-slide>19 <header>20 <h3>Slide three</h3>21 <p>Buttons disable at either end of the carousel.</p>22 </header>23 <p>Arrow keys page while the track is focused.</p>24 </article>25 <article class="card" data-carousel-slide>26 <header>27 <h3>Slide four</h3>28 <p>Slides can hold anything — cards, media, forms.</p>29 </header>30 <p>This one holds a .card article, like shadcn's demos.</p>31 </article>32 <article class="card" data-carousel-slide>33 <header>34 <h3>Slide five</h3>35 <p>The last slide. The next button is disabled here.</p>36 </header>37 <p>mo-carousel-change fires after each scroll settles.</p>38 </article>39 </div>40 <div data-carousel-controls>41 <button type="button" data-carousel-previous class="icon outline" aria-label="Previous slide">42 <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m12 19-7-7 7-7" /><path d="M19 12H5" /></svg>43 </button>44 <div data-carousel-dots></div>45 <button type="button" data-carousel-next class="icon outline" aria-label="Next slide">46 <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 12h14" /><path d="m12 5 7 7-7 7" /></svg>47 </button>48 </div>49 </section>50</mo-carousel>51
data-size="lg" gives each slide a 50% basis.
Two slides are visible at a time.
Snap points still land on slide starts.
Mixing sizes in one track is fine.
Custom bases via --mo-carousel-basis.
The last slide of the track.
1<mo-carousel>2 <section data-carousel aria-roledescription="carousel" aria-label="Multiple visible slides">3 <div data-carousel-track>4 <article class="card" data-carousel-slide data-size="lg">5 <header>6 <h3>One</h3>7 <p>data-size="lg" gives each slide a 50% basis.</p>8 </header>9 </article>10 <article class="card" data-carousel-slide data-size="lg">11 <header>12 <h3>Two</h3>13 <p>Two slides are visible at a time.</p>14 </header>15 </article>16 <article class="card" data-carousel-slide data-size="lg">17 <header>18 <h3>Three</h3>19 <p>Snap points still land on slide starts.</p>20 </header>21 </article>22 <article class="card" data-carousel-slide data-size="lg">23 <header>24 <h3>Four</h3>25 <p>Mixing sizes in one track is fine.</p>26 </header>27 </article>28 <article class="card" data-carousel-slide data-size="lg">29 <header>30 <h3>Five</h3>31 <p>Custom bases via --mo-carousel-basis.</p>32 </header>33 </article>34 <article class="card" data-carousel-slide data-size="lg">35 <header>36 <h3>Six</h3>37 <p>The last slide of the track.</p>38 </header>39 </article>40 </div>41 <div data-carousel-controls>42 <button type="button" data-carousel-previous class="icon outline" aria-label="Previous slide">43 <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m12 19-7-7 7-7" /><path d="M19 12H5" /></svg>44 </button>45 <div data-carousel-dots></div>46 <button type="button" data-carousel-next class="icon outline" aria-label="Next slide">47 <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 12h14" /><path d="m12 5 7 7-7 7" /></svg>48 </button>49 </div>50 </section>51</mo-carousel>52
Slides are 100% wide unless sized.
An 80% basis — a peek of the next slide.
A 50% basis shows the next half.
Bases can vary per slide.
Back to the 100% default.
1<mo-carousel>2 <section data-carousel aria-roledescription="carousel" aria-label="Slide sizes">3 <div data-carousel-track>4 <article class="card" data-carousel-slide>5 <header>6 <h3>Default</h3>7 <p>Slides are 100% wide unless sized.</p>8 </header>9 </article>10 <article class="card" data-carousel-slide data-size="sm">11 <header>12 <h3>data-size="sm"</h3>13 <p>An 80% basis — a peek of the next slide.</p>14 </header>15 </article>16 <article class="card" data-carousel-slide data-size="lg">17 <header>18 <h3>data-size="lg"</h3>19 <p>A 50% basis shows the next half.</p>20 </header>21 </article>22 <article class="card" data-carousel-slide data-size="sm">23 <header>24 <h3>Sized again</h3>25 <p>Bases can vary per slide.</p>26 </header>27 </article>28 <article class="card" data-carousel-slide>29 <header>30 <h3>Full width</h3>31 <p>Back to the 100% default.</p>32 </header>33 </article>34 </div>35 <div data-carousel-controls>36 <button type="button" data-carousel-previous class="icon outline" aria-label="Previous slide">37 <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m12 19-7-7 7-7" /><path d="M19 12H5" /></svg>38 </button>39 <div data-carousel-dots></div>40 <button type="button" data-carousel-next class="icon outline" aria-label="Next slide">41 <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 12h14" /><path d="m12 5 7 7-7 7" /></svg>42 </button>43 </div>44 </section>45</mo-carousel>46
data-spacing="tight" halves the track gap.
Two slides visible, 0.5rem apart.
Snap points still land on slide starts.
Override further via --mo-carousel-gap.
The last slide of the tight track.
1<mo-carousel>2 <section data-carousel data-spacing="tight" aria-roledescription="carousel" aria-label="Tight spacing">3 <div data-carousel-track>4 <article class="card" data-carousel-slide data-size="lg">5 <header>6 <h3>One</h3>7 <p>data-spacing="tight" halves the track gap.</p>8 </header>9 </article>10 <article class="card" data-carousel-slide data-size="lg">11 <header>12 <h3>Two</h3>13 <p>Two slides visible, 0.5rem apart.</p>14 </header>15 </article>16 <article class="card" data-carousel-slide data-size="lg">17 <header>18 <h3>Three</h3>19 <p>Snap points still land on slide starts.</p>20 </header>21 </article>22 <article class="card" data-carousel-slide data-size="lg">23 <header>24 <h3>Four</h3>25 <p>Override further via --mo-carousel-gap.</p>26 </header>27 </article>28 <article class="card" data-carousel-slide data-size="lg">29 <header>30 <h3>Five</h3>31 <p>The last slide of the tight track.</p>32 </header>33 </article>34 </div>35 <div data-carousel-controls>36 <button type="button" data-carousel-previous class="icon outline" aria-label="Previous slide">37 <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m12 19-7-7 7-7" /><path d="M19 12H5" /></svg>38 </button>39 <div data-carousel-dots></div>40 <button type="button" data-carousel-next class="icon outline" aria-label="Next slide">41 <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 12h14" /><path d="m12 5 7 7-7 7" /></svg>42 </button>43 </div>44 </section>45</mo-carousel>46
data-orientation="vertical" pages on the y axis.
The track becomes a column with a fixed height.
ArrowUp / ArrowDown page when focused.
Height via --mo-carousel-height (default 20rem).
Snap points land on slide starts.
1<mo-carousel>2 <section data-carousel data-orientation="vertical" aria-roledescription="carousel" aria-label="Vertical carousel">3 <div data-carousel-track>4 <article class="card" data-carousel-slide>5 <header>6 <h3>One</h3>7 <p>data-orientation="vertical" pages on the y axis.</p>8 </header>9 </article>10 <article class="card" data-carousel-slide>11 <header>12 <h3>Two</h3>13 <p>The track becomes a column with a fixed height.</p>14 </header>15 </article>16 <article class="card" data-carousel-slide>17 <header>18 <h3>Three</h3>19 <p>ArrowUp / ArrowDown page when focused.</p>20 </header>21 </article>22 <article class="card" data-carousel-slide>23 <header>24 <h3>Four</h3>25 <p>Height via --mo-carousel-height (default 20rem).</p>26 </header>27 </article>28 <article class="card" data-carousel-slide>29 <header>30 <h3>Five</h3>31 <p>Snap points land on slide starts.</p>32 </header>33 </article>34 </div>35 <div data-carousel-controls>36 <button type="button" data-carousel-previous class="icon outline" aria-label="Previous slide">37 <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m12 19-7-7 7-7" /><path d="M19 12H5" /></svg>38 </button>39 <div data-carousel-dots></div>40 <button type="button" data-carousel-next class="icon outline" aria-label="Next slide">41 <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 12h14" /><path d="m12 5 7 7-7 7" /></svg>42 </button>43 </div>44 </section>45</mo-carousel>46
USAGE
1<mo-carousel>2 <section data-carousel aria-roledescription="carousel" aria-label="Highlights">3 <div data-carousel-track>4 <article class="card" data-carousel-slide>…</article>5 </div>6 <div data-carousel-controls>7 <button type="button" data-carousel-previous class="icon outline">‹</button>8 <div data-carousel-dots></div>9 <button type="button" data-carousel-next class="icon outline">›</button>10 </div>11 </section>12</mo-carousel>
API
FILES
src/css/carousel.csssrc/js/carousel.jsmo add carouselTHEMING · PLAYGROUND
This is the exact component from the examples above, wearing a completely different theme. Edit the tokens and --mo-* hooks in the editor — autocomplete knows them all — and the preview obeys in real time. Your CSS always wins.
--foreground--radius--mo-carousel-basis--mo-carousel-dot--mo-carousel-gap--mo-carousel-gap-tight--mo-carousel-height--mo-carousel-size-lg--mo-carousel-size-smPreview
Your CSS
Try it: type --mo- for the component hooks, or edit any value — the preview obeys in real time.