npm package: @ivanalbizu/astro-swiperjs-webgl
Description: Astro component for WebGL image transitions using CurtainsJS and Swiper.js
Author: Iván Albizu
Latest version: 0.0.1

# @ivanalbizu/astro-swiperjs-webgl

Astro component for WebGL image transitions using displacement maps, powered by [CurtainsJS](https://www.curtainsjs.com/) and [Swiper.js](https://swiperjs.com/).

## Features

- WebGL transitions between images: displacement maps or particle scatter effect
- Thumbnail navigation carousel (Swiper.js)
- Per-slide title and content overlays using named slots
- Automatic performance detection with CSS fallback for low-end devices
- Respects `prefers-reduced-motion` user preference
- Compatible with Astro View Transitions (`<ClientRouter>`)
- Multiple independent instances on the same page
- Keyboard navigation scoped per instance
- Full accessibility: ARIA roles, labels, and focus management
- Customizable via props and CSS custom properties
- i18n support via `labels` prop
- Active thumbnail indicator
- TypeScript support

## Installation

```bash
npm install @ivanalbizu/astro-swiperjs-webgl
```

Peer dependencies (`curtainsjs` and `swiper`) are installed automatically with npm 7+. With pnpm, install them explicitly:

```bash
pnpm add @ivanalbizu/astro-swiperjs-webgl curtainsjs swiper
```

## Usage

### Basic

```astro
---
import { WebglSlider, WebglSliderImg } from '@ivanalbizu/astro-swiperjs-webgl';
---

<div style="width: 100%; height: 100vh;">
  <WebglSlider>
    <WebglSliderImg src="/img-01.jpg" alt="First image">
      <h2 slot="title">First Slide</h2>
      <p slot="content">Description for the first slide</p>
    </WebglSliderImg>
    <WebglSliderImg src="/img-02.jpg" alt="Second image">
      <h2 slot="title">Second Slide</h2>
      <p slot="content">Description for the second slide</p>
    </WebglSliderImg>
    <WebglSliderImg src="/img-03.jpg" alt="Third image" />
  </WebglSlider>
</div>
```

The slider fills its parent container. Set `width` and `height` on the wrapper element.

### With all props

```astro
<WebglSlider
  transition="displacement"
  displacement="/my-displacement-map.jpg"
  overlayPosition="bottom-left"
  overlayTheme="dark"
  transitionDamping={0.94}
  pixelRatio={2}
  navigation={true}
  loop={true}
  slidesPerView="auto"
  spaceBetween={20}
  ariaLabel="Product gallery"
  labels={{
    prevSlide: 'Previous',
    nextSlide: 'Next',
    slideLabel: '{current} of {total}',
  }}
>
  <WebglSliderImg src="/photo.jpg" alt="Photo description">
    <h2 slot="title">Title with <em>HTML</em></h2>
    <p slot="content">Rich content with <a href="#">links</a></p>
  </WebglSliderImg>
</WebglSlider>
```

### Custom navigation buttons

```astro
<button id="prev-btn" aria-label="Previous slide">Prev</button>
<button id="next-btn" aria-label="Next slide">Next</button>

<WebglSlider navigation={false} prevEl="#prev-btn" nextEl="#next-btn">
  <!-- slides -->
</WebglSlider>
```

## Components

### `<WebglSlider>`

Main slider wrapper. Accepts `<WebglSliderImg>` children.

### `<WebglSliderImg>`

Individual slide item with an image and optional named slots.

| Prop  | Type     | Required | Description         |
| :---- | :------- | :------- | :------------------ |
| `src` | `string` | Yes      | Image source URL    |
| `alt` | `string` | No       | Image alt text (`""`) |

**Named slots:**

- `title` — Slide title overlay (any HTML)
- `content` — Slide content overlay (any HTML)

## Props

All props for `<WebglSlider>`:

| Prop               | Type                  | Default              | Description                                    |
| :----------------- | :-------------------- | :------------------- | :--------------------------------------------- |
| `transition`       | `'displacement' \| 'particles'` | `'displacement'` | Transition type between slides                |
| `displacement`     | `string`              | `'/displacement4.jpg'` | Path to the displacement map image (only for `displacement` transition) |
| `overlayPosition`  | `OverlayPosition`     | `'bottom-left'`      | Position of the title/content overlay (9 positions) |
| `overlayTheme`     | `'dark' \| 'light'`   | `'dark'`             | Overlay background theme for contrast          |
| `transitionDamping`| `number`              | `0.96`               | Damping factor for transition speed (0-1). Lower = faster |
| `pixelRatio`       | `number`              | `1.5`                | WebGL pixel ratio (capped at `devicePixelRatio`) |
| `navigation`       | `boolean`             | `true`               | Show prev/next navigation buttons              |
| `prevEl`           | `string`              | —                    | CSS selector for external prev button          |
| `nextEl`           | `string`              | —                    | CSS selector for external next button          |
| `loop`             | `boolean`             | `true`               | Enable infinite loop (requires more than 3 slides) |
| `slidesPerView`    | `number \| 'auto'`    | `'auto'`             | Number of thumbnails visible                   |
| `spaceBetween`     | `number`              | `16`                 | Space between thumbnails (px)                  |
| `ariaLabel`        | `string`              | `'Image carousel'`   | Accessible label for the carousel region       |
| `labels`           | `SliderLabels`        | See below            | Customizable UI strings for i18n               |

## Overlay Position

The `overlayPosition` prop accepts 9 positions arranged in a 3×3 grid:

| | Left | Center | Right |
|:--|:--|:--|:--|
| **Top** | `top-left` | `top` | `top-right` |
| **Middle** | `left` | `center` | `right` |
| **Bottom** | `bottom-left` | `bottom` | `bottom-right` |

Bottom positions automatically account for the thumbnail bar height (`--wgl-swiper-height`).

The `overlayTheme` prop adds a semi-transparent background for contrast:
- `'dark'` — dark background (`rgba(0,0,0,0.6)`), white text
- `'light'` — light background (`rgba(255,255,255,0.8)`), dark text

```astro
<WebglSlider overlayPosition="top-right" overlayTheme="light">
```

## Labels

The `labels` prop allows customizing all UI strings, useful for i18n or personalization. All fields are optional — defaults are in English.

| Key          | Default                        | Description                                      |
| :----------- | :----------------------------- | :----------------------------------------------- |
| `prevSlide`  | `'Previous slide'`             | Aria label for prev button / screen reader       |
| `nextSlide`  | `'Next slide'`                 | Aria label for next button / screen reader       |
| `firstSlide` | `'This is the first slide'`    | Screen reader announcement on first slide        |
| `lastSlide`  | `'This is the last slide'`     | Screen reader announcement on last slide         |
| `slideLabel` | `'{current} / {total}'`        | Thumbnail aria-label template                    |

The `slideLabel` template supports `{current}` and `{total}` placeholders.

```astro
<WebglSlider
  labels={{
    prevSlide: 'Diapositiva anterior',
    nextSlide: 'Siguiente diapositiva',
    firstSlide: 'Primera diapositiva',
    lastSlide: 'Última diapositiva',
    slideLabel: '{current} de {total}',
  }}
>
```

## CSS Custom Properties

Override these on `.webgl-slider` or any parent to customize the appearance:

| Property                      | Default                        | Description                         |
| :---------------------------- | :----------------------------- | :---------------------------------- |
| `--wgl-z-index`               | `0`                            | Slider stacking context z-index     |
| `--wgl-overlay-color`         | `#fff`                         | Overlay text color                  |
| `--wgl-overlay-shadow`        | `0 2px 8px rgba(0, 0, 0, 0.6)` | Overlay text shadow                |
| `--wgl-overlay-padding`       | `24px`                         | Overlay distance from edges         |
| `--wgl-swiper-height`         | `110px`                        | Space reserved for thumbnails (bottom positions) |
| `--wgl-transition-duration`   | `0.4s`                         | Overlay fade in/out duration        |
| `--wgl-transition-offset`     | `12px`                         | Overlay slide-up offset             |
| `--wgl-thumb-border-color`    | `#fff`                         | Thumbnail border color              |
| `--wgl-thumb-border-radius`   | `3px`                          | Thumbnail border radius             |
| `--wgl-focus-outline-color`   | `#fff`                         | Focus-visible outline color         |

```css
.webgl-slider {
  --wgl-overlay-color: #1a1a1a;
  --wgl-thumb-border-color: transparent;
  --wgl-thumb-border-radius: 8px;
}
```

## Displacement Maps

The `displacement` prop accepts a path to a grayscale image that controls the transition distortion pattern. Different maps produce different visual effects (waves, ripples, geometric shapes, etc.).

Place your displacement map in the `public/` directory and reference it by path:

```astro
<WebglSlider displacement="/my-custom-displacement.jpg">
```

The image should be grayscale. Lighter areas produce more distortion during transitions.

## Astro Integration

### View Transitions

The component is fully compatible with Astro's `<ClientRouter>`. It automatically initializes on `astro:page-load` and cleans up on `astro:before-swap`, preventing memory leaks or duplicate instances during client-side navigation.

It also works on pages without View Transitions — initialization falls back to the `load` event.

### Performance Detection

On low-end devices or when the user prefers reduced motion, WebGL is skipped entirely. The component detects:

- `prefers-reduced-motion: reduce` media query
- Data saver mode (`navigator.connection.saveData`)
- Low device memory (`navigator.deviceMemory < 4`)
- Low CPU cores (`navigator.hardwareConcurrency <= 2`)

When any condition is met, images transition with a CSS opacity crossfade instead of WebGL shaders. The thumbnail carousel and overlay content continue to work normally.

### Accessibility

- `<section>` with `aria-roledescription="carousel"` as semantic landmark
- `aria-live="polite"` on the overlay for screen reader announcements
- Keyboard navigation: Arrow Left/Right from focused thumbnails or navigation buttons
- `focus-visible` outlines on thumbnails and navigation buttons
- Each thumbnail has `aria-label` with position (customizable via `labels.slideLabel`)

### Multiple Instances

Each slider is fully independent. Place as many `<WebglSlider>` components as needed — they do not interfere with each other's state, keyboard events, or WebGL contexts.

## License

[MIT](LICENSE) - Ivan Albizu