npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@present-day/carousel

v0.0.1

Published

Headless React carousel built on native CSS scroll snap

Readme

@present-day/carousel

Headless React carousel built on native CSS scroll snap. The browser owns the physics; the library owns state, navigation, and accessibility.

Why this and not Swiper/Embla?

  • Native physics, zero JS in the scroll path. Swiper/Embla reimplement momentum/snapping in JavaScript; this library lets CSS scroll snap do it, so swipes feel exactly like the platform because they ARE the platform.
  • Built on the 2026 primitives. Active-slide state comes from the browser's own scrollsnapchanging/scrollsnapchange events (Chrome/Edge 129+, Safari 18.2+) with a tiny fallback — not from reverse-engineered scroll math as the primary path.
  • Tiny. Zero runtime dependencies; a thin state/a11y layer over what the browser already does.
  • Headless. No bundled CSS, no theme to fight — style everything via classNames, including fully custom markers via renderMarker.
  • Accessible by default. WAI-ARIA carousel pattern: labeled slides, marker tablist with roving tabindex and keyboard nav, honest prefers-reduced-motion handling.

No infinite looping or autoplay. Looping requires scroll-teleport hacks to fake an infinite track; autoplay just needs a timer, but it's out of scope to keep this library tiny and headless. If you need either, use Embla.

Install

bun add @present-day/carousel
# or
npm i @present-day/carousel

Peer dependencies: react, react-dom (v18 or v19).

Basic usage

import { useState } from 'react'
import { Carousel } from '@present-day/carousel'

const [current, setCurrent] = useState(0)

<Carousel aria-label="Featured products" onSlideChange={setCurrent}>
  <Carousel.Slide className="slide">1</Carousel.Slide>
  <Carousel.Slide className="slide">2</Carousel.Slide>
  <Carousel.Slide className="slide">3</Carousel.Slide>
  <Carousel.Markers />
  <Carousel.Prev>←</Carousel.Prev>
  <Carousel.Next>→</Carousel.Next>
</Carousel>

Carousel.Markers, Carousel.Prev, and Carousel.Next are optional — mix in whichever controls you need, in any order, as siblings of the slides.

Slide sizing

Slides size themselves via CSS — the library never sets widths. width: 100% on your slide class gives one slide per page; width: 50% gives two.

.slide { width: 100%; } /* one per page */
.slide-half { width: 50%; } /* two per page */

Props

Carousel

| Prop | Type | Default | Description | | ---- | ---- | ------- | ----------- | | children | ReactNode | — | Carousel.Slide children plus any controls (Carousel.Markers, etc.) | | defaultSlide | number | 0 | Initial slide when uncontrolled | | slide | number | — | Controlled active slide index | | onSlideChange | (index: number) => void | — | Fires when the settled slide changes (after snap completes) | | orientation | 'horizontal' \| 'vertical' | 'horizontal' | Scroll axis | | align | 'start' \| 'center' | 'start' | scroll-snap-align of slides | | classNames | { root?, track?, slide? } | — | Class names for the root, track, and slides | | aria-label | string | — | Accessible name for the carousel region | | className | string | — | Added to the root element | | style | CSSProperties | — | Added to the root element |

Carousel.Slide

| Prop | Type | Default | Description | | ---- | ---- | ------- | ----------- | | children | ReactNode | — | Slide content | | className | string | — | Added to the slide element | | style | CSSProperties | — | Added to the slide element |

Carousel.Markers

| Prop | Type | Default | Description | | ---- | ---- | ------- | ----------- | | classNames | { list?, marker?, markerActive? } | — | Class names for the tablist, each marker, and the active marker | | renderMarker | (index: number, active: boolean) => ReactNode | — | Fully custom marker content | | aria-label | string | 'Slides' | Accessible name for the marker tablist |

Renders nothing when there's one slide or fewer.

Carousel.Prev / Carousel.Next

| Prop | Type | Default | Description | | ---- | ---- | ------- | ----------- | | children | ReactNode | — | Button content | | className | string | — | Added to the button | | style | CSSProperties | — | Added to the button | | aria-label | string | 'Previous slide' / 'Next slide' | Accessible name |

Both buttons disabled at their respective end of the carousel.

Vertical orientation

<Carousel
  aria-label="Story"
  orientation="vertical"
  classNames={{ track: 'vertical-track' }}
>
  <Carousel.Slide className="slide">1</Carousel.Slide>
  <Carousel.Slide className="slide">2</Carousel.Slide>
</Carousel>
.vertical-track { height: 240px; } /* required: constrains the scroll axis */

classNames.track must constrain the track's height, or there's nothing to scroll within. The track also sets overscroll-behavior on its scroll axis so a vertical carousel doesn't fight the page for scroll.

How it works

Three tiers of progressive enhancement, each falling back gracefully:

| Tier | Mechanism | Support | | ---- | --------- | ------- | | Physics | CSS scroll snap (scroll-snap-type, scroll-snap-align) | All browsers | | State | scrollsnapchanging / scrollsnapchange events | Chrome/Edge 129+, Safari 18.2+ — rAF scroll-math fallback elsewhere (Firefox, older Safari) | | Navigation | scrollTo({ behavior: 'smooth' }) | All browsers — instant under prefers-reduced-motion |

Constraints

Carousel.Slide must be a direct child of Carousel. Mapping an array of slides is fine; wrapping slides in your own components or fragments is not supported — the root partitions children by child.type === Slide.

Zero-JS dots

Supporting browsers can skip Carousel.Markers entirely and render dots with pure CSS:

/* Chrome/Edge 135+ — no React markers needed. `::scroll-marker` support is
   still landing across browsers; check current support before relying on it
   as your only navigation (caniuse: css-scroll-marker). */
.my-track {
  scroll-marker-group: after;
}
.my-track::scroll-marker-group {
  display: flex;
  gap: 8px;
  justify-content: center;
}
.my-slide::scroll-marker {
  content: '';
  width: 10px;
  height: 10px;
  border-radius: 50%;
  background: #ccc;
}
.my-slide::scroll-marker:target-current {
  background: #333;
}

Styling hooks

The root element carries data-active-slide={index} at all times, so you can target the active state from CSS without touching React:

[data-active-slide='0'] .progress { width: 20%; }

License

MIT