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

@minusplusmultiply/axis

v1.0.0

Published

A modern, accessible full-bleed slider for React built on native scrolling and CSS scroll-snap

Readme

Axis

Axis is a React slider for full-bleed horizontal layouts that keeps native browser scrolling and CSS scroll-snap intact.

npm License GitHub

When To Use It

Use Axis when you want:

  • full-bleed horizontal rails that align with your page container
  • native touch, trackpad, wheel, and keyboard scrolling
  • variable slide widths
  • built-in controls or fully custom headless composition
  • optional autoplay, thumbnails, virtualization, and infinite rails

Installation

pnpm add @minusplusmultiply/axis
npm install @minusplusmultiply/axis
yarn add @minusplusmultiply/axis

Import one stylesheet explicitly:

import "@minusplusmultiply/axis/styles";

@minusplusmultiply/axis/styles is the browser-ready default stylesheet.

import "@minusplusmultiply/axis/styles/vanilla";

@minusplusmultiply/axis/styles/vanilla is a compatibility import backed by the same built CSS asset.

If your app compiles Tailwind-authored CSS directly, use:

import "@minusplusmultiply/axis/styles/tailwind";

The package does not auto-import CSS.

If your app is Tailwind-first and you want stronger class conflict resolution, install tailwind-merge in the consuming app. Axis works without it.

Quick Start

import { Axis, AxisContent, AxisControl } from "@minusplusmultiply/axis";
import "@minusplusmultiply/axis/styles";

export function FeaturedWorkSlider() {
  return (
    <Axis aria-label="Featured work" containerMax={1200} gutter="px-6" gap={24}>
      <AxisContent>
        <article className="w-[320px] rounded-2xl bg-neutral-900 p-6 text-white">
          Project One
        </article>
        <article className="w-[460px] rounded-2xl bg-neutral-900 p-6 text-white">
          Project Two
        </article>
        <article className="w-[320px] rounded-2xl bg-neutral-900 p-6 text-white">
          Project Three
        </article>
      </AxisContent>
      <AxisControl variant="paddles" />
    </Axis>
  );
}

CSS-Only / No-JS

If you only want the base layout, scroll behavior, and scroll-snap styling, you can use the compiled stylesheet with plain markup and no React enhancements.

import "@minusplusmultiply/axis/styles";
<section
  class="axis"
  data-axis-layout="full"
  data-axis-align="start"
  style="
    --axis-container-max: 1200px;
    --axis-page-gutter: 1.5rem;
    --axis-gap: 24px;
  "
>
  <div class="axis-slider">
    <div class="axis-track">
      <div class="axis-slide"><article style="width: 320px;">Slide 1</article></div>
      <div class="axis-slide"><article style="width: 460px;">Slide 2</article></div>
      <div class="axis-slide"><article style="width: 320px;">Slide 3</article></div>
    </div>
  </div>
</section>

CSS-only mode includes:

  • full-bleed and contained layout math
  • native horizontal scrolling
  • CSS scroll-snap
  • slide spacing and width constraints

CSS-only mode does not include:

  • JS controls behavior
  • autoplay
  • active-slide state
  • virtualization
  • infinite rebasing
  • keyboard enhancements

Core Patterns

Sizing examples

<Axis containerMax={1600} gutter={2} gap={32} />
<Axis containerMax="--container-max" gutter="--page-edge" gap="--card-gap" />
<Axis containerMax="max-w-7xl" gutter="px-8" gap="gap-6" />
<Axis containerMax="7xl" gutter="8" gap="6" />

Layout modes

// Full-bleed viewport (default)
<Axis layout="full" containerMax={1200} gutter="px-6" gap={24} />

// Parent-contained viewport (no viewport-edge bleed)
<Axis layout="contained" containerMax={1200} gutter="px-6" gap={24} />

In contained mode, Axis uses the parent/root width first and still caps the content width with containerMax.

| Option | Type | Default | Applies to | Notes | | -------- | ---------------------- | -------- | ---------- | ---------------------------------------------------------------------- | | layout | "full" | "contained" | "full" | Axis | full is viewport-bleed, contained keeps viewport in parent bounds. |

Standard controls

<Axis aria-label="Case studies" containerMax={1200} gutter="px-6" gap={24}>
  <AxisContent>
    {slides.map((slide) => (
      <article key={slide.id} className="w-[360px]">
        {slide.title}
      </article>
    ))}
  </AxisContent>

  <AxisControl variant="paddles" />
  <AxisControl variant="dots" />
</Axis>

AxisControl supports paddles, dots, left, right, and thumbnails.

External controls

import { useRef } from "react";
import {
  Axis,
  AxisContent,
  AxisControl,
  type AxisControlsBinding,
} from "@minusplusmultiply/axis";
import "@minusplusmultiply/axis/styles";

export function ExternalControlsExample() {
  const controlsRef = useRef<AxisControlsBinding | null>(null);

  return (
    <>
      <header className="flex justify-end gap-3">
        <AxisControl controls={controlsRef} variant="left" />
        <AxisControl controls={controlsRef} variant="right" />
        <AxisControl controls={controlsRef} variant="dots" />
      </header>

      <Axis
        aria-label="Selected projects"
        containerMax={1200}
        controlsRef={controlsRef}
        gutter="px-6"
        gap={24}
      >
        <AxisContent>
          {slides.map((slide) => (
            <article key={slide.id} className="w-[320px]">
              {slide.title}
            </article>
          ))}
        </AxisContent>
      </Axis>
    </>
  );
}

Headless composition

import {
  Axis,
  AxisPagination,
  AxisSlide,
  AxisTrack,
  AxisViewport,
  useAxisSlider,
} from "@minusplusmultiply/axis";
import "@minusplusmultiply/axis/styles";

function SliderStatus() {
  const slider = useAxisSlider();
  return (
    <p>
      {slider.activeIndex + 1} / {slider.slideCount}
    </p>
  );
}

export function HeadlessExample() {
  return (
    <Axis aria-label="Featured work" containerMax={1200} gutter="px-6" gap={24}>
      <AxisViewport>
        <AxisTrack>
          <AxisSlide index={0}>Slide 1</AxisSlide>
          <AxisSlide index={1}>Slide 2</AxisSlide>
          <AxisSlide index={2}>Slide 3</AxisSlide>
        </AxisTrack>
      </AxisViewport>

      <AxisPagination />
      <SliderStatus />
    </Axis>
  );
}

Advanced Patterns

Autoplay

<Axis autoplay />
<Axis autoplay={{ interval: 4000, mode: "loop" }} />
<Axis autoplay={{ interval: 4000, mode: "bounce", pauseOnHover: true }} />

Supported autoplay modes are stop, loop, and bounce.

Autoplay is opt-in. When enabled, Axis pauses on focus and hover by default and exposes play, pause, and toggleAutoplay through the reactive and imperative control APIs so you can render an explicit pause/play button.

| Option | Type | Default | Applies to | Notes | | ------------------------- | --------- | ------------------------------ | --------------- | ---------------------------------------------------------------------- | | autoplay | boolean | AxisAutoplayOptions | false | Axis | | enabled | boolean | true when object is provided | Axis autoplay | Explicit on/off flag for object form. | | interval | number | 5000 | Axis autoplay | Delay between autoplay navigation steps in milliseconds. | | mode | "stop" | "loop" | "bounce" | "stop" | | pauseOnHover | boolean | true | Axis autoplay | Pauses while the pointer is over the root. | | pauseOnFocus | boolean | true | Axis autoplay | Pauses while focus is inside the root. | | pauseOnInteraction | boolean | true | Axis autoplay | Pauses autoplay during user navigation or direct scroll interaction. | | autoResumeOnInteraction | boolean | true | Axis autoplay | When true, autoplay resumes automatically after interaction settles. | | interactionResumeDelay | number | 150 | Axis autoplay | Delay in milliseconds before auto-resume after interaction. | | pauseWhenHidden | boolean | true | Axis autoplay | Pauses when the document becomes hidden. | | startOnMount | boolean | true | Axis autoplay | Starts autoplay immediately after mount when enabled. |

Thumbnail controls

<AxisControl
  variant="thumbnails"
  thumbnailItems={[
    { content: <img src="/thumb-1.jpg" alt="" />, ariaLabel: "Show slide 1" },
    { content: <img src="/thumb-2.jpg" alt="" />, ariaLabel: "Show slide 2" },
    { content: <img src="/thumb-3.jpg" alt="" />, ariaLabel: "Show slide 3" },
  ]}
/>

| Option | Type | Default | Applies to | Notes | | ---------------------------- | --------------------------------------------------------------------------- | ----------------------------- | ------------- | -------------------------------------------------------------- | | variant | "thumbnails" | none | AxisControl | Enables thumbnail-button rendering. | | thumbnailItems | Array<{ key?: React.Key; content: React.ReactNode; ariaLabel?: string; }> | Generated numeric items | AxisControl | Supply custom thumbnail content and labels per slide. | | thumbnailClassName | string | "" | AxisControl | Applied to every thumbnail button. | | activeThumbnailClassName | string | "" | AxisControl | Added when the thumbnail matches the current pagination index. | | inactiveThumbnailClassName | string | "" | AxisControl | Added to non-active thumbnail buttons. | | renderThumbnail | (props) => React.ReactNode | none | AxisControl | Full custom renderer for each thumbnail button. | | controls | AxisControlsBinding | RefObject<AxisControlsBinding | null> | null |

Virtualized slide content

<AxisContent
  virtualize={{ overscan: 1, estimateSize: () => 320 }}
  slideCount={100}
  renderSlide={({ index }) => <Card index={index} />}
/>

Virtualization preserves all slide wrappers and only limits mounted slide content.

renderSlide receives additive state metadata:

  • isActive
  • isVisible
  • isMeasured
  • estimatedWidth
  • measuredWidth

| Option | Type | Default | Applies to | Notes | | ------------------- | ---------------------------------------------------- | ---------------------- | ------------------------ | -------------------------------------------------------------------------------- | | virtualize | boolean | AxisVirtualizeOptions | false | AxisContent | | overscan | number | 1 | AxisContent virtualize | Renders extra slides before and after the visible range. | | estimateSize | (index: number) => number | string | () => "auto" | AxisContent virtualize | | keepMountedActive | boolean | true | AxisContent virtualize | Keeps the active slide content mounted even if it falls outside the window. | | slideCount | number | none | AxisContent | Required for virtualized rendering. | | renderSlide | (params: AxisRenderSlideParams) => React.ReactNode | none | AxisContent | Required for virtualized rendering; receives logical and physical slide indexes. |

Unknown-size content and measurement

Axis treats async or unknown-width content as a geometry problem rather than a separate lazy-slide mode.

  • non-virtualized rails tolerate unknown widths best
  • virtualized rails should provide estimates when possible
  • infinite rails work best with deterministic estimates
  • strict infinite mode can defer rebasing until geometry is trustworthy
<AxisContent
  measurement={{
    estimate: (index) => widths[index] ?? 320,
    cache: "memory",
    strategy: "strict",
    strictInfinite: true,
  }}
  virtualize={{ overscan: 1, estimateSize: () => "auto" }}
  slideCount={slides.length}
  renderSlide={({ index, isMeasured, measuredWidth }) => (
    <Card data-ready={isMeasured} data-width={measuredWidth} index={index} />
  )}
/>

| Option | Type | Default | Applies to | Notes | | ---------------- | ------------------------ | -------------------------- | ------------- | ------------------------------------------------------------------ | | measurement | AxisMeasurementOptions | none | AxisContent | Configures fallback estimation, caching, and loop-safety strategy. | | estimate | number | ((index: number) => number | "auto") | virtualizer estimate | | cache | "memory" | false | "memory" | measurement | | strategy | "tolerant" | "strict" | "tolerant" | measurement | | strictInfinite | boolean | true in infinite mode | measurement | Prevents silent loop rebasing until strict geometry checks pass. |

Root callbacks and readiness

Axis exposes additive callbacks so consumers can react to geometry and autoplay state without inferring from DOM timing:

| Option | Type | Applies to | Notes | | ----------------------- | ---------------------------------- | ---------- | --------------------------------------------------------------------- | | aria-labelledby | string | Axis | Alternative to aria-label for heading-linked carousel labelling. | | onMeasurementChange | (measurementByIndex) => void | Axis | Fires when logical measurement state changes. | | onGeometryStabilized | (measurementByIndex) => void | Axis | Fires once all logical slides are at least estimated for the session. | | onAutoplayStateChange | (isAutoplaying: boolean) => void | Axis | Fires when autoplay enters or leaves its active state. |

Infinite slider

<AxisContent
  infinite
  virtualize={{ overscan: 1, estimateSize: (index) => widths[index] }}
  slideCount={slides.length}
  renderSlide={({ index }) => <Card index={index} />}
/>

Infinite mode is designed for virtualized slide rendering and works best when slide widths can be estimated accurately. If geometry remains low-confidence and measurement.strategy is "strict", Axis exposes canSafelyLoop = false and defers silent rebasing.

| Option | Type | Default | Applies to | Notes | | ------------------- | ---------------------------------------------------- | ---------------------- | ---------------------- | ------------------------------------------------------------------------ | | infinite | boolean | AxisInfiniteOptions | false | AxisContent | | copiesPerSide | number | 2 | AxisContent infinite | Number of repeated copy bands rendered on each side of the logical set. | | recenterThreshold | number | "viewport" | "viewport" | AxisContent infinite | | virtualize | boolean | AxisVirtualizeOptions | required in practice | AxisContent | | slideCount | number | none | AxisContent | Required because the logical slide count must be known. | | renderSlide | (params: AxisRenderSlideParams) => React.ReactNode | none | AxisContent | Required for infinite mode; receives logical and physical slide indexes. |