@minusplusmultiply/axis-slider
v1.0.13
Published
A modern, accessible, full-bleed slider component for React with CSS scroll-snap
Maintainers
Readme
Axis Slider
A modern, accessible, full-bleed horizontal slider designed for NextJS with native CSS scroll-snap.
Features
- Full-bleed layout — Slides extend edge-to-edge while content stays aligned to your container
- Variable slide widths — Each slide can have its own width
- Native scroll-snap — Smooth, GPU-accelerated scrolling with native touch/swipe
- Keyboard navigation — Arrow keys, Home, End
- Accessible — Full ARIA support, screen reader announcements, reduced motion support
- SSR-safe — Works with Next.js, Remix, and other SSR frameworks
- Flexible sizing — Use numbers, CSS variables, or Tailwind classes
- Customizable controls — Dots, paddles, or build your own with render props
- Imperative API — Control the slider programmatically via ref
Installation
npm install @minusplusmultiply/axis-sliderpnpm add @minusplusmultiply/axis-slideryarn add @minusplusmultiply/axis-sliderImport Styles
With Tailwind CSS:
import "@minusplusmultiply/axis-slider/styles";Without Tailwind (vanilla CSS):
import "@minusplusmultiply/axis-slider/styles/vanilla";Optional: Tailwind Merge
For intelligent Tailwind class merging, install tailwind-merge:
npm install tailwind-mergeAxis Slider automatically uses it when available.
Quick Start
import { Axis, AxisSlides, AxisControl } from "@minusplusmultiply/axis-slider";
import "@minusplusmultiply/axis-slider/styles";
export default function MySlider() {
return (
<Axis containerMax={1200} gutter="px-6" gap={24}>
<AxisSlides>
<div className="w-[320px] bg-neutral-800 p-6 rounded-xl">Slide 1</div>
<div className="w-[480px] bg-neutral-800 p-6 rounded-xl">Slide 2</div>
<div className="w-[320px] bg-neutral-800 p-6 rounded-xl">Slide 3</div>
</AxisSlides>
<AxisControl variant="paddles" />
</Axis>
);
}Components
<Axis>
The main container. Manages state, provides context, and handles keyboard navigation.
<Axis
containerMax={1200} // Max content width (px, CSS var, or Tailwind class)
gutter="px-6" // Horizontal padding (rem, CSS var, or Tailwind class)
gap={24} // Gap between slides (px, CSS var, or Tailwind class)
debug={false} // Show layout debug outlines
aria-label="Image gallery"
onSlideChange={(index) => console.log("Active:", index)}
onScrollStart={() => console.log("Scrolling...")}
onScrollEnd={() => console.log("Stopped")}
>
{children}
</Axis><AxisSlides>
The scrollable slide container. Each direct child becomes a slide.
<AxisSlides className="py-4">
<Card className="w-[300px]">...</Card>
<Card className="w-[400px]">...</Card>
<Card className="w-[300px]">...</Card>
</AxisSlides><AxisControl>
Navigation controls. Choose from dots, paddles, or individual buttons.
// Pagination dots
<AxisControl variant="dots" />
// Previous/Next buttons
<AxisControl variant="paddles" />
// Individual buttons (for custom positioning)
<AxisControl variant="left" className="absolute left-4 top-1/2" />
<AxisControl variant="right" className="absolute right-4 top-1/2" />Flexible Sizing
All size props accept multiple formats:
// Raw numbers (px for containerMax/gap, rem for gutter)
<Axis containerMax={1600} gutter={2} gap={32} />
// CSS variables
<Axis containerMax="--container-max" gutter="--edge" gap="--gap" />
// Tailwind classes
<Axis containerMax="max-w-7xl" gutter="px-8" gap="gap-6" />
// Tailwind tokens
<Axis containerMax="7xl" gutter="8" gap="6" />Imperative API
Control the slider programmatically using a ref:
import { useRef } from "react";
import { Axis, AxisSlides, type AxisRef } from "@minusplusmultiply/axis-slider";
export default function MySlider() {
const sliderRef = useRef<AxisRef>(null);
return (
<>
<button onClick={() => sliderRef.current?.goToPrevious()}>Prev</button>
<button onClick={() => sliderRef.current?.goToNext()}>Next</button>
<button onClick={() => sliderRef.current?.goToIndex(0)}>First</button>
<Axis ref={sliderRef}>
<AxisSlides>
<div className="w-[300px]">Slide 1</div>
<div className="w-[300px]">Slide 2</div>
<div className="w-[300px]">Slide 3</div>
</AxisSlides>
</Axis>
</>
);
}AxisRef Methods
| Method | Returns | Description |
| ------------------ | --------- | ------------------------------ |
| goToIndex(n) | void | Navigate to slide at index n |
| goToNext() | void | Navigate to next slide |
| goToPrevious() | void | Navigate to previous slide |
| getActiveIndex() | number | Get current active slide index |
| getSlideCount() | number | Get total number of slides |
| isAtStart() | boolean | Check if at first slide |
| isAtEnd() | boolean | Check if at last slide |
Custom Controls
Use render props for full control over navigation UI:
Custom Dots
<AxisControl
variant="dots"
renderDot={({ isActive, index, buttonProps }) => (
<button {...buttonProps} className={isActive ? "bg-white" : "bg-white/30"}>
{index + 1}
</button>
)}
/>Custom Buttons
<AxisControl
variant="paddles"
previousContent={<ChevronLeftIcon />}
nextContent={<ChevronRightIcon />}
buttonClasses="p-3 rounded-full bg-black/50 hover:bg-black/70"
/>
// Or with full render control:
<AxisControl
renderButton={({ type, isDisabled, buttonProps }) => (
<button {...buttonProps} className="my-button">
{type === "previous" ? <ChevronLeft /> : <ChevronRight />}
</button>
)}
/>useAxisControls Hook
Build completely custom controls using the useAxisControls hook:
import { useAxisControls } from "@minusplusmultiply/axis-slider";
function CustomControls() {
const { goToNext, goToPrevious, activeIndex, slideCount, atStart, atEnd } =
useAxisControls();
return (
<div className="flex items-center gap-4">
<button onClick={goToPrevious} disabled={atStart}>
←
</button>
<span>
{activeIndex + 1} / {slideCount}
</span>
<button onClick={goToNext} disabled={atEnd}>
→
</button>
</div>
);
}
// Use inside <Axis>
<Axis>
<AxisSlides>...</AxisSlides>
<CustomControls />
</Axis>;Props Reference
Axis Props
| Prop | Type | Default | Description |
| --------------- | ------------------------- | ---------- | ------------------------------- |
| containerMax | number \| string | 960 | Max content width |
| gutter | number \| string | 1.5 | Horizontal padding (rem) |
| gap | number \| string | 24 | Gap between slides (px) |
| debug | boolean | false | Show debug outlines |
| aria-label | string | "Slider" | Accessible label |
| onSlideChange | (index: number) => void | — | Fires when active slide changes |
| onScrollStart | () => void | — | Fires when scrolling starts |
| onScrollEnd | () => void | — | Fires when scrolling ends |
AxisControl Props
| Prop | Type | Default | Description |
| ----------------------- | ------------------------------------------ | ----------- | ----------------------------- |
| variant | "dots" \| "paddles" \| "left" \| "right" | "paddles" | Control type |
| className | string | — | Container classes |
| dotClasses | string | — | Classes for all dots |
| activeDotClasses | string | — | Classes for active dot |
| inactiveDotClasses | string | — | Classes for inactive dots |
| buttonClasses | string | — | Classes for all buttons |
| disabledButtonClasses | string | — | Classes for disabled buttons |
| previousButtonClasses | string | — | Classes for previous button |
| nextButtonClasses | string | — | Classes for next button |
| previousContent | ReactNode | "←" | Previous button content |
| nextContent | ReactNode | "→" | Next button content |
| renderDot | function | — | Custom dot render function |
| renderButton | function | — | Custom button render function |
Accessibility
Axis Slider is built with accessibility in mind:
- Keyboard navigation — Arrow keys, Home, End
- ARIA attributes — Proper roles, labels, and live regions
- Screen reader support — Announces slide changes
- Focus management — Logical focus order
- Reduced motion — Respects
prefers-reduced-motion
Browser Support
- Chrome, Firefox, Safari, Edge (modern versions)
- CSS scroll-snap (95%+ browser support)
- Graceful degradation — works as a scrollable container without JavaScript
- SSR-safe — compatible with Next.js, Remix, Astro, etc.
Performance
- Native scroll-snap — GPU-accelerated, 60fps scrolling
- Passive event listeners — No scroll jank
- Minimal footprint — ~20KB minified, no dependencies
- No animation libraries — Pure CSS transitions
- Native touch support — Swipe works out of the box
License
MIT © Minus Plus Multiply
