npm package: @minusplusmultiply/axis-slider
Description: A modern, accessible, full-bleed slider component for React with CSS scroll-snap
Author: Minus Plus Multiply / James Hooper
Latest version: 1.0.13

# Axis Slider

A modern, accessible, full-bleed horizontal slider designed for NextJS with native CSS scroll-snap.

[![npm version](https://img.shields.io/npm/v/@minusplusmultiply/axis-slider.svg)](https://www.npmjs.com/package/@minusplusmultiply/axis-slider)
[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
[![GitHub](https://img.shields.io/badge/GitHub-minusplusmultiply%2Faxis--slider-181717?logo=github)](https://github.com/minusplusmultiply/axis-slider)
[![Website](https://img.shields.io/badge/Website-minusplusmultiply.com-000?logo=vercel)](https://www.minusplusmultiply.com)

---

## 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

```bash
npm install @minusplusmultiply/axis-slider
```

```bash
pnpm add @minusplusmultiply/axis-slider
```

```bash
yarn add @minusplusmultiply/axis-slider
```

### Import Styles

**With Tailwind CSS:**

```tsx
import "@minusplusmultiply/axis-slider/styles";
```

**Without Tailwind (vanilla CSS):**

```tsx
import "@minusplusmultiply/axis-slider/styles/vanilla";
```

### Optional: Tailwind Merge

For intelligent Tailwind class merging, install `tailwind-merge`:

```bash
npm install tailwind-merge
```

Axis Slider automatically uses it when available.

---

## Quick Start

```tsx
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.

```tsx
<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.

```tsx
<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.

```tsx
// 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:

```tsx
// 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:

```tsx
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

```tsx
<AxisControl
  variant="dots"
  renderDot={({ isActive, index, buttonProps }) => (
    <button {...buttonProps} className={isActive ? "bg-white" : "bg-white/30"}>
      {index + 1}
    </button>
  )}
/>
```

### Custom Buttons

```tsx
<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:

```tsx
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](https://www.minusplusmultiply.com)

---

## Links

- [Website](https://www.minusplusmultiply.com)
- [GitHub Repository](https://github.com/minusplusmultiply/axis-slider)
- [NPM Package](https://www.npmjs.com/package/@minusplusmultiply/axis-slider)
- [Report Issues](https://github.com/minusplusmultiply/axis-slider/issues)