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.1.2

Published

A native-first React slider for lightweight, accessible horizontal carousels with container-aligned edge-to-edge layouts.

Readme

Axis

Axis progressively enhances native scrolling with controls, autoplay, synced sliders, and virtualized content. The slider remains native, with CSS scroll-snap handling alignment.

Variable-width slides can run edge to edge while their content aligns with the rest of the page. Axis supplies the scrolling behaviour and minimal control styles; you supply the design.

Get started

npm install @minusplusmultiply/axis

Requires React 18 or 19 and React DOM. There are no other runtime dependencies. Import the stylesheet once in your application:

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

export function Gallery() {
  return (
    <Axis aria-label="Selected projects" container={1200} gutter="20px" gap={24}>
      <AxisContent>
        <article style={{ width: 320, padding: 24 }}>Project one</article>
        <article style={{ width: 440, padding: 24 }}>Project two</article>
        <article style={{ width: 320, padding: 24 }}>Project three</article>
      </AxisContent>
      <AxisControl paddles />
      <AxisControl dots />
    </Axis>
  );
}

Static children render on the server and remain available without JavaScript. Hydration adds controls and other enhancements to the native scroller.

Upgrading from 1.0.x

Version 1.1.1 includes two intentional compatibility changes:

  • Replace imports and calls of useAxisControls with useAxisSlider; the old hook export has been removed.
  • Axis now preserves application class strings without resolving conflicting Tailwind utilities. Supply non-conflicting classes or merge them in your application.

All stylesheet subpaths remain supported and now contain the same browser-ready CSS in the components cascade layer. Import only one stylesheet.

Layout and styling

| Setting | Purpose | Default | | --- | --- | --- | | container | Maximum page container width | 960px | | gutter | Page edge inset | 1.5rem | | gap | Space between slides | 24px | | layout | "full" for edge-to-edge scrolling; "contained" within the content column | "full" | | align | Slide alignment: "start", "center", or "end" | "start" |

Numeric container and gap values are pixels; numeric gutter values are rem. Explicit CSS lengths avoid ambiguity. Existing utility-style tokens and CSS custom properties are also supported:

<Axis container={1200} gutter="20px" gap={24} />
<Axis container="--page-width" gutter="--page-inset" gap="--card-gap" />
<Axis container="max-w-7xl" gutter="px-6" gap="gap-6" />

Use container and gutter for page alignment, and gap for slide spacing. Do not add padding, margins, spacing utilities, or spacer slides to the rail. In contained mode, the content column is already aligned; an additional gutter inside the track would apply the inset twice. Padding inside individual cards is fine.

Your CSS or Tailwind

Tailwind is optional. Axis calculates layout through CSS variables and ships browser-ready CSS in the components cascade layer. Ordinary application CSS can style cards and controls.

For Tailwind 4, establish its layer order and import Axis after Tailwind in your application stylesheet:

@layer theme, base, components, utilities;
@import "tailwindcss";
@import "@minusplusmultiply/axis/styles";

Then pass classes to your cards or control styling props:

<AxisControl
  paddles
  buttonClasses="rounded-full bg-black text-white"
  disabledButtonClasses="opacity-30"
/>

Axis preserves class strings; it does not resolve conflicting utilities. Supply non-conflicting classes, or use your application's class-merging helper. The older styles/vanilla and styles/tailwind imports resolve to the same CSS; import only one.

Controls

<AxisControl paddles />
<AxisControl dots />
<AxisControl previous />
<AxisControl next />
<AxisControl thumbnails thumbnailItems={[
  { content: <img src="/one.jpg" alt="" />, ariaLabel: "Show project one" },
  { content: <img src="/two.jpg" alt="" />, ariaLabel: "Show project two" },
]} />

Choose one control type per component. Omitting a selector renders paddles; false flags are ignored. Previous and next arrows follow LTR or RTL direction. Dots and thumbnails follow the logical pagination position.

Use className for the wrapper, buttonClasses for paddles, dotClasses for dots, and thumbnailClassName for thumbnails. Selected-state props and renderButton, renderDot, or renderThumbnail let you use your own components. Spread the supplied button props to preserve actions and accessibility.

Controls can live outside the slider:

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

function GalleryWithToolbar() {
  const controlsRef = useRef<AxisControlsBinding | null>(null);
  return (
    <>
      <div>
        <AxisControl controls={controlsRef} previous />
        <AxisControl controls={controlsRef} next />
      </div>
      <Axis aria-label="Selected projects" controlsRef={controlsRef}>
        <AxisContent>
          <article style={{ width: 320 }}>Project one</article>
          <article style={{ width: 440 }}>Project two</article>
        </AxisContent>
      </Axis>
    </>
  );
}

Expanding pagination indicators

Developers choose the presentation independently of autoplay:

<AxisControl dots />                  // Ordinary dots
<AxisControl dots pill />             // Expanding active marker
<AxisControl dots autoplayProgress /> // Active marker with timed fill

These options also work on AxisPagination. autoplayProgress implies pill; neither starts autoplay. Enable playback on Axis separately. Play/pause UI is also optional: compose it separately with useAxisNavigation().toggleAutoplay; progress indicators never insert a playback button. The fill follows the engine's actual interval, fades out during pauses and native scrolling, and starts a fresh interval when playback resumes. The active marker remains visible when stopped. Reduced motion suppresses the fill animation.

The active indicator expands as neighbouring dots slide smoothly into place. The group keeps a fixed width and centre, including during interrupted transitions. Reduced motion makes these changes immediate. The expanded indicator has square corners by default; set --axis-dot-radius: 999px for rounded ends. --axis-dot-inactive-width and --axis-dot-height allow thin bars instead of dots.

Use CSS variables to style the indicators. Expanding controls have 44px minimum button targets by default:

.my-pagination {
  --axis-dot-target: 44px;
  --axis-dot-gap: 8px;
  --axis-dot-size: 8px;
  --axis-dot-active-width: 32px;
  --axis-dot-color: #888;
  --axis-dot-active-color: #333;
  --axis-dot-progress-color: #fff;
  --axis-dot-radius: 0;
  --axis-dot-reset-duration: 180ms;
  --axis-dot-transition: 280ms;
}

Pass className="my-pagination" to the control. Use --axis-dot-gap for spacing so the reserved group width stays in sync. For closer dots, reduce the target width and retain a taller click area:

.my-pagination {
  --axis-dot-target: 32px;
  --axis-dot-gap: 0px;
}
.my-pagination .axis-dot {
  height: 44px;
}

The fill follows LTR/RTL direction. .axis-dot-indicator and .axis-dot-progress are available for custom CSS. Custom dot renderers should preserve buttonProps.children (included when spreading the props) to keep the built-in indicator, or replace it with their own content. For custom timing displays, useAxisSlider().autoplayTiming provides { startedAt, duration } in milliseconds using the performance.now() clock, or null while stopped; older/custom bindings may omit it. Timing updates occur at interval boundaries, not every animation frame.

Pagination overflow

Dot pagination uses overflow="scroll" by default. When the row exceeds its parent's available width, it scrolls horizontally without widening the page. The row stays centred when it fits. The current or keyboard-focused button is kept in view, including in RTL, without moving the page or the slide rail.

<AxisControl dots pill overflow="scroll" />
<AxisControl dots pill overflow="hide" />
<AxisPagination autoplayProgress overflow="hide" />

overflow="hide" hides the entire row when it cannot fit, removes it from keyboard navigation and the accessibility tree, and collapses its height. It reappears when the parent grows or the slide count decreases enough to fit. This option works with ordinary dots, pills, and autoplay progress. Paddles and thumbnails are unaffected. Compose paddles alongside hidden pagination when you want buttons to remain available.

The .axis-pagination wrapper owns overflow; className and dot styling variables still apply to the inner .axis-dots group. Before client measurement (or without ResizeObserver), hide mode falls back to a scrollable row.

Overscroll

Axis allows native edge feedback by default. Set overscroll={false} to suppress it:

<Axis overscroll>{/* Slides */}</Axis>
<Axis overscroll={false}>{/* Slides */}</Axis>

This uses CSS overscroll-behavior-x: contain when enabled and none when disabled. Both prevent horizontal scroll chaining into a parent scroller. The browser and device determine the edge effect; Axis does not simulate a bounce. This leaves layout and scroll-snap unchanged. scrollBlock still suppresses direct horizontal input independently. Nested sliders keep their own overscroll setting.

Autoplay

Autoplay is off by default. Enable it with autoplay or provide options:

<Axis autoplay={{ interval: 4000, mode: "bounce", startOnMount: false }} />

Modes are linear (stop at the end), loop (rewind to the start), bounce (reverse direction), and random (choose another slide). Loop uses native scrolling, not a seamless infinite track.

Playback pauses for hover, focus, interaction, a hidden document, and an offscreen slider by default. Reduced motion prevents automatic startup; users can explicitly start playback. Always provide a pause control:

import { useAxisNavigation, useAxisState } from "@minusplusmultiply/axis";

function PlayPause() {
  const { play, pause } = useAxisNavigation();
  const { isAutoplayRequested } = useAxisState();
  return (
    <button type="button" onClick={isAutoplayRequested ? pause : play}>
      {isAutoplayRequested ? "Pause slides" : "Play slides"}
    </button>
  );
}

Place this component inside Axis. Use isAutoplayRequested for the button label: it retains playback intent during temporary pauses. isAutoplaying reports current activity. Defaults are a 10-second interval, linear mode, and automatic resumption after interaction; set autoResumeOnInteraction: false to require an explicit restart.

More options

Synced sliders

Give sliders the same syncGroup. Choose syncStrategy="index" to match logical items, "equivalent" to match relative scroll progress, or "step" to follow settled pagination changes. Group controls use <AxisControl syncGroup="gallery" paddles />; grouped dots and thumbnails are unsupported.

Virtualized content

<AxisContent
  slideCount={100}
  virtualize={{ overscan: 1, estimateSize: () => 320 }}
  renderSlide={({ index }) => (
    <article style={{ width: 320 }}>Project {index + 1}</article>
  )}
/>

Virtualization keeps slide wrappers in place and mounts nearby bodies. Focused content stays mounted; save editable values in application state. With renderSlide, windowing is enabled by default. Use virtualize={false} to render every body, including on the server.

Measured widths are cached and refreshed when content remounts or resizes. measurement={{ cache: false }} disables that cache. Supply estimates for unknown widths. Appending and removing terminal slides is supported; change the React key on AxisContent when replacing or reordering the dataset because cached state is index-based.

Custom UI

Use useAxisState() for common state, useAxisNavigation() for actions, useAxisPaginationState() for pagination, or useAxisSelector(selector, equality?) for a specific subscription. useAxisSlider() returns the complete state and methods.

Counters, dots, and thumbnails should use paginationIndex. activeIndex identifies the slide nearest the alignment anchor and can differ near the rail end. Both are zero-based.

For custom markup, compose AxisViewport, AxisTrack, and indexed AxisSlide components inside Axis. AxisPagination supplies pagination independently of AxisControl. Keep the viewport, track, and slide structure intact.

Horizontal input guard

Set scrollBlock on Axis to suppress direct horizontal input while keeping vertical page scrolling. Controls and programmatic navigation remain available. The standalone useScrollBlock hook is exported from @minusplusmultiply/axis/scrollblock.

Accessibility

Use aria-labelledby when a visible heading labels the slider, or aria-label otherwise. The native viewport is keyboard-focusable before hydration; enhanced navigation supports Arrow, Home, and End keys while leaving input controls to handle their own keys.

Keep visible focus styles and specific button labels. Provide a pause button when enabling autoplay. For content that must remain fully available without JavaScript, use static children or disable virtualization.

Tree-shaking

Import the exports you use from the ESM package with a production bundler. Unused components and hooks can be removed; importing a primitive such as AxisSlide does not include the scrolling engine. The config and scrollblock entry points also work independently.

Import the stylesheet explicitly. CSS is retained as a side effect, and all three stylesheet paths contain the same CSS. Axis does not remove individual CSS selectors based on component usage. CommonJS remains supported, but the same tree-shaking guarantees do not apply. Runtime choices inside a component—such as which control AxisControl renders—are not separate bundle entry points; use AxisPagination when you only need pagination.


−+× · Minus Plus Multiply

MIT licensed. See LICENSE and NOTICE in the package.