swiper-dial
v0.4.0
Published
Text slider addon for Swiper: hang the words on a dial, a ring, a fan, a 3D barrel or a straight rule. 280 presets
Maintainers
Readme
swiper-dial
A text slider for Swiper that puts the list on a track. Words radiating from a hub like spokes, running around a ring the way type sits on a stamp, spread across a fan, wrapped onto a 3D barrel - or laid along a straight rule, which is the same treatment measured in px instead of degrees. The live word sits at the anchor and reads straight; the rest turn away, fade, shrink and - if you want it - disappear behind a mask.
It draws the track too: a hairline struck at the line the words ride on - an arc from the hub on a wheel, a rule on a straight one - optionally marked with a station for every entry, so the layout reads as a mechanism rather than a set of angled words.
It is a Swiper module, not a widget. loop, freeMode, autoplay,
mousewheel, a vertical direction and Swiper's own .swiper-slide-active class
all keep working, because the plugin only reads Swiper's result and writes one
transform per slide.
280 presets in fourteen families, zero dependencies beyond Swiper, one JS file.
import Swiper from 'swiper';
import { DialEffect } from 'swiper-dial';
import 'swiper/css';
import 'swiper-dial/css';
new Swiper('.swiper', {
modules: [DialEffect],
slidesPerView: 'auto',
centeredSlides: true,
loop: true,
dial: { layout: 'dial', radius: 130, window: 150 },
});How it works
- On every
setTranslatethe plugin reads where Swiper has just put each slide- the same
swiperSlideOffsetSwiper's own effects use - and works out where the middle of the viewport falls between two slides. That fractional index, not a pixel distance, is what drives the angle.
- the same
- Each slide gets one
transform: a translate that brings its top-left corner to the hub, the plane'srotateX/rotateY, and the polar pair that puts it on the wheel. - Distance from the anchor - measured in degrees, not pixels - drives the opacity, the scale, the blur, the stacking order and whether the word is hidden altogether.
- The container gets whatever it needs to hold the result: room across the strip, a perspective origin on the hub, the conic mask and the guide's SVG.
The strip does not move
Swiper normally slides the whole wrapper and lets the slides ride along. Here it
does not: the plugin turns on virtualTranslate, holds the strip at zero and
reads the translate as a number to place from.
That matters for how it looks, not just for how it is built. If the wrapper moved too, each word would be the sum of two animations - Swiper's along the strip and the wheel's around the hub - and those two only agree at the ends of a transition. In between they drift apart, and the words slide sideways and settle back instead of simply turning. With the strip held still, the only thing that changes between one word and the next is an angle, so a transition is a pure rotation about the hub.
The container also reserves its room from the wheel's worst case rather than from wherever the drag has stopped, so the box never breathes mid-swipe.
Why the fractional index matters
Words are not cards. "Logos" and "Data Visualizations" are laid out at very
different widths, and a wheel spaced by pixels would bunch the short words
together and strand the long ones. Counting in slides gives every word the same
slice of the turn, whatever it says - so slidesPerView: 'auto' with
width: auto slides is the natural setup, and the wheel still comes out even.
Install
Bundler (Vite / webpack / Next)
npm i swiper swiper-dialimport Swiper from 'swiper';
import { Autoplay } from 'swiper/modules';
import { DialEffect } from 'swiper-dial';
import 'swiper/css';
import 'swiper-dial/css';
new Swiper('.swiper', {
modules: [DialEffect, Autoplay],
slidesPerView: 'auto',
centeredSlides: true,
loop: true,
grabCursor: true,
autoplay: { delay: 2000, disableOnInteraction: false },
dial: {
layout: 'dial',
radius: 130,
window: 150,
mask: { enabled: true, spread: 132, feather: 28 },
},
});Swiper has been modular since v9: without importing Autoplay / FreeMode /
Mousewheel and listing them in modules, those options are silently ignored.
CDN, no build step
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/swiper@14/swiper-bundle.min.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/swiper-dial@0/dist/swiper-dial.css">
<script src="https://cdn.jsdelivr.net/npm/swiper@14/swiper-bundle.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/swiper-dial@0/dist/swiper-dial.iife.js"></script>
<script>
new Swiper('.swiper', {
modules: [SwiperDialEffect],
slidesPerView: 'auto',
centeredSlides: true,
loop: true,
dial: { layout: 'dial', radius: 130, window: 150 },
});
</script>The IIFE build puts the module on window.SwiperDialEffect.
The markup
A plain carousel. The slides are words, not cards:
<div class="swiper">
<div class="swiper-wrapper">
<div class="swiper-slide">Posters</div>
<div class="swiper-slide">Brand Identities</div>
<div class="swiper-slide">Motion Graphics</div>
</div>
</div>The stylesheet that ships with the plugin makes each slide hug its own text
(width: auto; height: auto; white-space: nowrap) and sets the transform origin
the wheel is measured from. That is all it does - the type is yours, and the
type is the layout: the wheel is sized from the box each word occupies.
.swiper {
font: 500 18px/1.2 ui-monospace, monospace;
letter-spacing: 0.06em;
text-transform: uppercase;
}
.swiper-slide { color: #6b6b70; cursor: pointer; transition: color 0.3s; }
.swiper-slide-active { color: #fff; } /* Swiper's own class */Two classes the plugin adds, for the cases CSS does better:
| Class | On |
| --- | --- |
| .swiper-dial-hidden | words pushed outside window - already invisible and click-through |
| .swiper-dial-back | words on the far half of a closed wheel, i.e. the ones reading upside down unless flip is on |
Layouts
| layout | What it does |
| --- | --- |
| dial | Words radiate from the hub like spokes; the live one lies flat at 3 o'clock. align says whether a word starts at the radius and runs outwards, ends there, or straddles it. |
| ring | Words sit along the circle rather than across it, the way type runs round a stamp. The live one is at 12 o'clock; flip turns the bottom half the right way up. |
| fan | A dial cut down to one wedge: the list is spread over fan.spread degrees and stops instead of closing, so the first and last words are ends rather than neighbours. |
| barrel | The strip is wrapped onto a 3D drum turning about the drag axis. A vertical carousel gives you the picker from a phone, a horizontal one a wheel of fortune. |
| line | The track is straight. Everything else here still applies, only read in px along the rule instead of degrees around a hub - see A straight track. |
anchor is measured from each layout's own zero, so 0 always means "the live
word reads straight where you would expect it". Everything else - radius,
step, window, the falloff, the mask - works the same way in all four.
Parameters
Everything lives under dial in the normal Swiper config, and every value can be
changed on a running instance.
Layout
| Path | Default | What it does |
| --- | --- | --- |
| enabled | true | turn the whole effect off |
| layout | 'dial' | 'dial' / 'ring' / 'fan' / 'barrel' |
| radius | 200 | radius of the wheel in px; for barrel, how far the drum stands off the axis |
| fit | 0 | radius as a fraction of half the shorter side of the box. 0 leaves radius in charge |
| step | 0 | degrees between neighbouring words. 0 works it out: the full turn split between the slides, or fan.spread for a fan |
| anchor | 0 | where the live word sits, in degrees from the layout's own zero |
| reverse | false | turn the wheel the other way as the carousel advances |
| align | 'out' | dial / fan only: 'out', 'in' or 'center' on the spoke |
| origin | 'anchor' | dial / fan only: whether hub marks where the live word starts or the centre of the wheel |
| upright | false | keep every word horizontal; they still travel round, they just never turn |
| flip | false | turn the words on the far half the right way up |
| window | 0 | total arc that stays on screen, in degrees. 0 shows the whole wheel |
| box | 'auto' | how the container handles a wheel taller than a line of text |
| respectReducedMotion | false | skip the effect entirely under prefers-reduced-motion |
hub - where the wheel is pinned
| Path | Default | |
| --- | --- | --- |
| hub.x | 0.5 | horizontal position as a fraction of the box |
| hub.y | 0.5 | vertical position as a fraction of the box |
| hub.dx | 0 | nudge across, in px |
| hub.dy | 0 | nudge down, in px |
What that point is depends on origin. On a dial or a fan it defaults to
'anchor': the place the live word starts, with the centre of the wheel worked
out backwards, a whole radius away in the other direction. So radius reads as
"how much the list bends" rather than "how far right everything flies", and
turning it up flattens the arc without moving the word you are looking at.
origin: 'hub' pins the centre of the wheel instead. A ring and a barrel
always work that way - there the centre is the only handle that makes sense.
fan - only for layout: 'fan'
| Path | Default | |
| --- | --- | --- |
| fan.spread | 140 | total arc the list is spread over, in degrees |
plane - tip the whole wheel in 3D
The hub stays put; the plane it lives on turns.
| Path | Default | |
| --- | --- | --- |
| plane.tilt | 0 | rotateX, in degrees. Lays the wheel back like a record on a table |
| plane.turn | 0 | rotateY, in degrees. Turns it away like an opening door |
| plane.perspective | 900 | perspective on the container, in px. Lower = stronger foreshortening |
falloff - how a word changes as it turns away
Driven by the angle from the anchor, normalised against window when one is set
and against the whole half-turn (or fan.spread / 2) when it is not.
| Path | Default | |
| --- | --- | --- |
| falloff.fade | 0.6 | opacity lost at the far end, 0..1 |
| falloff.scale | 0 | how much it shrinks there, 0..1 |
| falloff.blur | 0 | blur there, in px |
| falloff.power | 1 | exponent of the ramp. Higher = the change hugs the far end |
| falloff.stack | true | stack the words by distance from the anchor, nearest on top |
dof - depth of field
falloff.blur is a ramp from the anchor outwards. This is a lens: a focal plane
you can move anywhere on the wheel, a sharp zone either side of it and a
front/back bias - so the list reads as a photograph with one word in focus.
Everything is in degrees along the wheel, and it rides on top of falloff.
| Path | Default | |
| --- | --- | --- |
| dof.enabled | false | |
| dof.focus | 0 | focal plane, in degrees from the anchor. 0 puts the live word in focus |
| dof.range | 20 | half-width of the sharp zone, in degrees. Inside it nothing blurs |
| dof.blur | 12 | blur at the far end of the arc, in px |
| dof.falloff | 1.6 | how fast the blur ramps up outside the sharp zone |
| dof.dim | 0.2 | extra dimming that rides with the blur, 0..1 |
| dof.scale | 0 | extra shrink that rides with the blur, 0..1 |
| dof.bias | 0 | which side blurs. 0 both, 1 only past the focus, -1 only in front of it |
Moving focus off the anchor is what makes it worth having: at -40 the words
sharpen as they arrive and soften once they have passed, which reads as a rack
focus following the list.
guide - the track
The line the words run on: one <svg> the plugin owns, inserted behind the
strip and deaf to the mouse. A barrel has no arc to draw - its words travel in
depth rather than around a centre - so it ignores this.
| Path | Default | |
| --- | --- | --- |
| guide.enabled | false | |
| guide.gap | 12 | clearance between the track and the words, in px. Ignored when they straddle the line (align: 'center', upright, a ring) |
| guide.offset | 0 | raw radial nudge, in px. Negative draws the line inside the radius |
| guide.spread | 0 | how much of the wheel to draw, in degrees. 0 follows the run of words on screen |
| guide.width | 1 | stroke width. 0 draws no line, leaving only the marks |
| guide.color | 'currentColor' | any CSS colour |
| guide.opacity | 0.35 | |
| guide.dash | 0 | dash length in px, with an equal gap. 0 is solid |
| guide.taper | false | fade the line out towards both of its ends |
| guide.ticks | 0 | a mark per word, struck across the track at that word's angle. Length in px |
| guide.tickWidth | 0 | stroke width of the marks. 0 follows width |
| guide.tickActive | 1 | how much longer the live word's mark is |
| guide.activeColor | '' | colour for the live station. Empty follows color |
| guide.activeOpacity | 0 | opacity for it. 0 leaves it on the same ramp as the rest |
| guide.drift | 0 | how fast the dashes travel against the words: 0 nailed to the track, 1 running with the list, -1 against it |
The marks fade towards the ends of the arc along with the words, so a dimmed
word has a dimmed station. With width: 0 and ticks on you get the scale
without the rail.
None of the three is a switch. How much of the reading a mark is holding is dealt out by how near it is to the anchor, so as the wheel turns one station grows, brightens and takes the colour while its neighbour gives them up - the mark hands over rather than jumping. The change rides the same transition Swiper gives the slides, and each mark keeps its own place between frames so there is a previous value for the browser to animate from.
The marks are struck at a fixed pitch along the track and the whole set is turned by however far the wheel has turned, so a station stays under its word through a drag, a transition and a resize alike. That turn takes the same transition Swiper gives the slides, so the pattern travels with the words rather than arriving before them.
drift moves the dashes and nothing else. The arc never moves; it is the
texture on it that runs, so -1 sends the dashes crawling against the list
while every station stays exactly where its word is. The marks are deliberately
left out of it - a station sitting between two words reads as a mistake, however
deliberate it was.
mask - the wedge
A slice of pie cut out of the container, centred on the anchor and feathered along both cuts. This masks the container, so it takes whatever else is inside it too.
| Path | Default | |
| --- | --- | --- |
| mask.enabled | false | |
| mask.spread | 120 | angle of the fully visible slice, in degrees |
| mask.feather | 24 | width of the fade on each side of it, in degrees |
window and mask overlap on purpose. window removes words outright - they
stop being drawn and stop taking clicks. mask is a soft edge that catches
half-visible words mid-turn. Most of the presets that use one use both, with the
mask a little wider than the window.
marquee - a wheel that never stops
autoplay steps: it moves, eases out, waits, moves again, and the pause is the
point of it - one word at a time, each given a moment to be read. A marquee is
the other thing. It runs at one speed and never arrives, so there is no easing
to feel and no station to rest on, and what you read is whatever is passing.
| Path | Default | |
| --- | --- | --- |
| marquee.enabled | false | |
| marquee.speed | 0.35 | stations a second; negative runs it the other way |
| marquee.hover | true | hold while the pointer is over the carousel |
loop: true, // required: a strip with two ends would reach one
dial: {
marquee: { enabled: true, speed: 0.5 },
}It needs loop, and it turns itself off under prefers-reduced-motion outright
rather than optionally - everything else here moves because a reader asked it
to, and this moves on its own and does not stop. Dragging pauses it and letting
go resumes it, so the hand always wins. Leave autoplay off: the two want
opposite things and will fight over the same strip.
sound - the click of the detent
A real dial is not silent: it clicks as each station passes the detent, and that click is what tells your hand it moved a notch rather than drifting. The click here is tied to the same fractional index the layout is built on, so drag slowly and the ticks come slowly, fling it and they run.
Nothing is sampled. A click is a short burst of noise through a resonant filter, built in code, so this costs no assets and adds no dependency.
| Path | Default | |
| --- | --- | --- |
| sound.enabled | false | |
| sound.character | 'detent' | detent, rotary, watch, latch, typewriter, woodblock, chime, geiger, or custom |
| sound.volume | 0.25 | loudness at walking pace, 0 to 1 |
| sound.hover | true | a light tick as a word comes under the pointer |
| sound.press | true | a fuller sound when a word is pressed |
| sound.velocity | 0.6 | how much speed brightens and lifts the click, 0 to 1 |
| sound.tone | 1800 | centre frequency in Hz - character: 'custom' only |
| sound.body | 8 | how narrowly the filter rings, ~1 to 30 - custom only |
| sound.decay | 22 | length of a click in ms - custom only |
The three sounds are one voice in three registers, not three separate noises: the hover is the same mechanism heard lighter and higher, the press lower and fuller. That is what makes a set of interface sounds read as one object rather than as a soundboard.
A named character owns tone, body and decay; volume and velocity stay
yours either way, because they are about level and feel rather than about what
the mechanism is made of.
dial: {
sound: { enabled: true, character: 'rotary', volume: 0.3 },
}Two things are deliberate. It is off by default - a library that makes noise on its own is a bug. And the audio context is built on the reader's first touch or keypress, never on load, which is also what browsers require: audio cannot start without a gesture.
loop needs a long enough strip
loop is Swiper's, not this plugin's, and it builds its blocks from the real
slides: it needs the strip comfortably longer than the container. Below that it
warns once in the console and then quietly lays slides on top of each other,
which on a wheel reads as words clumping in one sector and springing apart
again.
With slidesPerView: 'auto' a wide container can measure fifteen words across,
so a list of twenty cannot satisfy it. The dial places every word itself, so
what Swiper thinks fits across the strip has no bearing on the picture - which
means you can simply tell it a smaller number:
slidesPerView: 5, // not 'auto': the loop only needs the strip short
loop: true,The exception is layout: 'line', which takes its spacing from the words' own
widths and would be flattened by the equal widths a numeric value imposes.
There, keep 'auto' and give the list enough entries instead.
Room for the wheel
A wheel is taller than the line of text it grew out of, so something has to give.
| box | |
| --- | --- |
| 'auto' | The plugin measures the wheel and reserves exactly enough room across the strip, with the hub where hub puts it. A fan hanging below its hub asks for less than the same fan centred on it. The default. |
| 'clip' | The box is left alone and Swiper's own clipping applies. For a section that is already the size you want. |
| 'free' | The box is left alone and nothing is clipped: the wheel runs over the section. Give it room around the carousel. |
fit is the one case 'auto' stays out of: there the radius is read from the
box, so growing the box to fit the wheel would only grow the wheel again. Give
those a height of your own.
A vertical carousel needs a height whatever the setting - Swiper cannot measure a vertical strip inside a box that has none.
Runtime API
swiper.dial.setParams({ radius: 320 }); // live, next frame
swiper.dial.setParams({ falloff: { fade: 1 } }); // deep-merged
swiper.dial.refresh(); // after swapping slides by hand
swiper.dial.params; // current state, fully resolvedEvery parameter is read fresh on the next frame, so anything here is free to animate.
A straight track
layout: 'line' takes the hub away and leaves everything else standing. The
live word still sits at the anchor, the rest still fade, shrink, blur and pass
behind a mask, the rule is still drawn with a station under every word - all of
it measured in px along the track instead of degrees around a centre.
Six options change their unit and nothing else: step, anchor, window,
dof.focus, dof.range and the mask's spread and feather. Two are the
line's own:
| Option | Default | What it does |
| --- | --- | --- |
| line.angle | 0 | Angle of the track itself. 0 runs it left to right, 90 top to bottom, anything between lays it on a diagonal. |
| line.offset | 0 | Where the words sit across the track, in px. The straight-line answer to align, which only has a radius to work from. |
step left at 0 follows the longest word, measured along the track - so a
rule stood on end is spaced by how tall the words are and a diagonal one by a
bit of both. The stations stay even the way a dial's spokes do; a step you set
yourself overrides it.
loop behaves the way it does everywhere else in Swiper. With it on the track
is endless: the distance wraps on the length of the strip exactly as a wheel's
angle wraps on 360, so a word running off one end comes back on the other and
there are always words either side of the anchor. Turn it off and the first and
last words become real ends, the way they do on a fan.
dial: {
layout: 'line',
falloff: { fade: 0.75, scale: 0.25 },
guide: { enabled: true, ticks: 10 },
}Presets
280 configurations in fourteen families, importable on their own:
import { presets, presetFamilies } from 'swiper-dial/presets';
new Swiper('.swiper', {
modules: [DialEffect],
...presets['dial-wedge'].swiper, // some presets set the carousel up too
dial: presets['dial-wedge'].dial,
});| Family | 20 presets each |
| --- | --- |
| dial | Words on spokes, the live one flat at the anchor. The signature layout. |
| ring | Words along the circle, the way type sits on a stamp or a coin. |
| fan | An arc that spreads and stops: the whole list opens out of one corner. |
| barrel | The strip on a 3D drum - a phone picker or a wheel of fortune. |
| menu | Dials built to be navigated: small radii, edge anchors, readable wedges. |
| hero | One big statement per section. These set the type size too. |
| ticker | Wheels that turn on their own. These set the carousel up as well. |
| orbit | The wheel as an object in space: tilted, turned, in perspective. |
| quiet | Long radii and tiny steps - a word swap that happens to be on a wheel. |
| stack | Dense wheels for long lists, tuned so a dozen words stay legible. |
| lens | One word in focus and the rest defocused by a movable focal plane. |
| guide | The track drawn in, from a hairline to a marked-up gauge face. |
| instrument | Track, stations and lens together: gauges, scales and read-outs. |
| line | The hub taken away: the same falloff, lens, mask and rule on a straight track. |
The control schema behind the tuning panel on the demo site is exported too, if you want to build your own:
import { effectControls, defaultsOf, toDialParams } from 'swiper-dial/controls';Recipes
The radial list from the screenshot
dial: {
layout: 'dial',
radius: 130,
window: 150,
mask: { enabled: true, spread: 132, feather: 28 },
}A phone picker
new Swiper('.swiper', {
modules: [DialEffect],
direction: 'vertical',
slidesPerView: 7,
centeredSlides: true,
loop: true,
dial: {
layout: 'barrel',
radius: 110,
step: 20,
falloff: { fade: 0.85, power: 1.3 },
},
});.swiper { height: 320px } - a vertical carousel needs one.
A navigation dial in the left margin
dial: {
layout: 'dial',
radius: 150,
upright: true, // words stay level, so they stay clickable
window: 130,
hub: { x: 0.2, y: 0.5 },
mask: { enabled: true, spread: 118, feather: 26 },
}A rack focus
dial: {
layout: 'dial',
radius: 160,
window: 170,
falloff: { fade: 0.35 },
dof: {
enabled: true,
focus: -40, // the plane sits above the anchor
range: 14, // a narrow sharp zone
blur: 14,
dim: 0.2,
},
}A gauge face
dial: {
layout: 'dial',
radius: 180,
window: 110,
guide: {
enabled: true,
width: 2,
ticks: 16, // a station for every word
tickActive: 2.2, // the live one, twice as long
opacity: 0.4,
},
}A read-out: track, stations and lens together
dial: {
layout: 'dial',
radius: 160,
window: 150,
falloff: { fade: 0.5 },
guide: {
enabled: true,
ticks: 16, // a station for every word
tickActive: 2.4, // the reading, longer
width: 2,
opacity: 0.45,
},
dof: {
enabled: true,
range: 20, // sharp either side of the reading
blur: 14,
bias: 1, // only what is past it softens
},
}Type round a badge
dial: {
layout: 'ring',
fit: 0.8, // radius from the box, so give the box a size
flip: true, // the bottom half reads the right way up
box: 'clip',
falloff: { fade: 0.2 },
}Things to know
loop and the seam
loop recycles slides by moving them from one end of the strip to the other,
and a slide that moves changes its place in the order by the whole list at once.
The plugin hides that for you, in the only two places it can show:
- On a closed wheel a word is never drawn more than half a turn from the
anchor. When
stepis worked out from the count, one list is exactly one turn and the angle wraps onto itself, so nothing was ever visible - but astepyou set yourself does not divide the turn, and without the cut the recycled words swap seats in plain sight. - On any layout, the word more than half a list away from the live one is
hidden outright. That is precisely the one
loopis about to move, so the swap happens off-screen and a fan simply grows a word at one end as it drops one from the other.
Both cuts land where the wheel has already doubled back on itself, so they cost nothing you could see.
loop needs a strip to work with
This is Swiper's own constraint, but a text wheel walks into it more easily than a carousel of cards: words are narrow, so a short list of short words can leave the strip barely longer than the container, and Swiper then quietly fails to build the loop blocks. The symptom is unmistakable - the wheel stops turning at the seam and the arrows look dead.
Give it room: more entries, or more spaceBetween. Space costs nothing here,
because the wheel is spaced by index rather than by pixels - the gap only feeds
Swiper's arithmetic and the feel of the drag.
Slides have to size themselves
slidesPerView: 'auto' and the stylesheet's width: auto are not a
suggestion. The wheel is measured from the box each word occupies, so a numeric
slidesPerView - which makes Swiper stamp every slide to the same width - would
centre and align the boxes rather than the words inside them.
On small screens
radius is in px, so it does not shrink with the viewport. Either move the hub
(hub.x closer to the edge leaves the words more room to run into), drop the
radius in a media query through swiper.dial.setParams(), or hand the sizing
over to the box with fit.
Notes
- Swiper 11+. Developed against v14. The plugin uses only the public module
hooks (
beforeInit,setTranslate,setTransition,update,resize,loopFix) and theswiperSlideOffset/slidesSizesGridlayout data that Swiper's own effects read. virtualTranslateis on (except undercssMode, which cannot be virtualised). The slides stay in Swiper's normal flow - so loop, free mode and variable widths all behave - but the strip itself is held still and the wheel does all the moving.- Transforms and opacity only. No layout is written per frame, so the wheel animates on the compositor.
transform-style: preserve-3dis applied to the wrapper only while a 3D parameter is in play (barrel,plane.tilt,plane.turn), so a flat wheel keeps its plain 2D stacking.- Reduced motion. The wheel is a static layout rather than an animation, so
nothing turns unless the user turns it.
respectReducedMotion: truedrops the effect entirely and leaves a plain row of words. - TypeScript.
swiper-dial.d.tsmergesdialintoSwiperOptions, so it type-checks inside a normal Swiper config with no setup call.
Development
npm install
npm run dev # demo site on :3004
npm run build:lib # dist/swiper-dial.iife.js + dist/swiper-dial.css
npm run build # the demo site, into dist-site/src/swiper-dial.js is the whole library. Everything else in src/ is either
the stylesheet, the types, the preset and control data, or the demo site under
src/app/.
License
MIT © Artur Grzeda
