@ivanalbizu/astro-swiperjs-webgl
v0.0.1
Published
Astro component for WebGL image transitions using CurtainsJS and Swiper.js
Maintainers
Readme
@ivanalbizu/astro-swiperjs-webgl
Astro component for WebGL image transitions using displacement maps, powered by CurtainsJS and Swiper.js.
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-motionuser 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
labelsprop - Active thumbnail indicator
- TypeScript support
Installation
npm install @ivanalbizu/astro-swiperjs-webglPeer dependencies (curtainsjs and swiper) are installed automatically with npm 7+. With pnpm, install them explicitly:
pnpm add @ivanalbizu/astro-swiperjs-webgl curtainsjs swiperUsage
Basic
---
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
<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
<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
<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.
<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 |
.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:
<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: reducemedia 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>witharia-roledescription="carousel"as semantic landmarkaria-live="polite"on the overlay for screen reader announcements- Keyboard navigation: Arrow Left/Right from focused thumbnails or navigation buttons
focus-visibleoutlines on thumbnails and navigation buttons- Each thumbnail has
aria-labelwith position (customizable vialabels.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 - Ivan Albizu
