Slider
FancySlider is a reusable slider/carousel built on
Embla Carousel. One component covers a card
carousel, an autoplaying showcase, synced sliders, and more, driven by
plain props and slots. Each slide is a FancySlide whose content slot
takes any content you like.
Prerequisites
None — it's self-contained. The Embla library is inlined in the component's script, so the slider works whether or not the experimental Etch Carousel plugin is installed. If the plugin is present, its copy of Embla is used instead; either way there's nothing to install.
The autoplay progress bar is driven by a plain CSS variable
(--fe-slider-progress, 0→1) that the script advances — no @keyframes,
so there's no "Fancy Slider" stylesheet to paste and nothing to set up
before using autoplay.
Anatomy
FancySlider (the container) holds two slots:
slides— oneFancySlideper slide.controls— the controls you want, in the order and position you want. Nothing is wired in by default: add only what the design needs.
Controls are separate components, so they can go anywhere — inside the slot, above the slides, in a corner, or outside the slider entirely:
| Component | What it does |
|---|---|
FancySliderArrow | Previous or next button (direction prop). |
FancySliderDots | Pagination. Skin dots / progress / lines / worm; colors and sizes are set from its properties (no CSS needed). |
FancySliderThumbnails | A thumbnail strip — one preview per slide, click to jump, active one highlighted. Pulls each slide's image automatically; see Thumbnail navigation. |
FancySliderPlayPause | Play/pause for autoplay. Can also start autoplay on a slider that has it switched off. |
FancySliderProgress | The autoplay progress bar. |
FancySliderCounter | Current-slide counter — fraction (2 / 5) or current (just the number). Skin inline / stacked / pill, plus Colors, Size, Separator and zero-pad, all from the panel. Updates live. |
FancySliderControls | Optional row that lays a group of controls out horizontally. |
The script binds to data-* attributes rather than classes, so any design
can restyle the slider freely without touching behavior:
| Attribute | Where | Purpose |
|---|---|---|
data-fe-slider | Root | Marks the slider. All options are read from plain attributes on this same element. |
data-fe-slider-viewport | Overflow clip | Embla's viewport — the element that clips the moving track. |
data-fe-slider-container | The moving track | Holds the slides; this is what actually translates. |
data-fe-slider-slide | Each slide | One FancySlide. |
data-fe-slider-prev / -next | Buttons | Previous / next controls. |
data-fe-slider-dots | Wrapper | The script fills this with one dot button per page. |
data-fe-slider-thumbnails | Wrapper | The script fills this with one thumbnail button per slide. |
data-fe-slider-playpause | Button | Play/pause toggle (autoplay only). |
data-fe-slider-progress | Bar | The autoplay progress bar. |
Props
FancySlider
The properties panel is organized into four collapsible groups.
Label (a11y) — label, the accessible name for the whole carousel,
sits on its own at the top.
Accent color — accentColor (default var(--primary)) themes the
controls at once: every control's color props default to
var(--fe-slider-accent, var(--primary)), so this sets the dots' active
color, the progress fill, arrow/play hover, etc. in one place. A per-control
color still overrides it, and a control placed outside the slider falls back
to var(--primary).
Layout
| Prop | Type | Default | Notes |
|---|---|---|---|
slidesMobile | select | "1" | Slides visible at once on mobile (portrait, below 478px). |
slidesLandscape | select | "1" | Slides from the landscape-phone breakpoint up (478px). Defaults to 1 so it matches the mobile count until you set it apart. |
slidesTablet | select | "2" | Slides visible from the tablet breakpoint up. |
slidesDesktop | select | "3" | Slides visible from the desktop breakpoint up. |
viewportBreakpoints | boolean | false | Which width the counts above react to. Off (default): the slider's own width (container query) — the same slider shows fewer slides in a narrow column than at full width. On: the window width (media query). See Responsive breakpoints. |
slidesToScroll | select | "1" | How many slides advance per navigation step. |
snapPosition | select | "start" | Where the active slide comes to rest inside the viewport — start, center, end. This is about scroll position, not visual alignment. center/end show from the first slide (only meaningful when slides are narrower than the viewport). |
slideAlign | select | "stretch" | How slides line up when their content has different heights — stretch makes them all as tall as the tallest; start, center and end let each keep its natural height and align it to the top, middle or bottom. |
orientation | select | "horizontal" | Scroll direction — horizontal or vertical. See Vertical orientation. |
height | text | "22rem" | Viewport height, vertical only (horizontal takes its height from the slides). |
width | text | "28rem" | Max width, vertical only — a vertical track has no natural width, so without a cap the widest slide (an image at its intrinsic size) would define it. |
Behavior
| Prop | Type | Default | Notes |
|---|---|---|---|
loop | boolean | true | Endless scrolling — wraps around at the ends. |
transition | select | "normal" | Scroll animation speed — slow, normal, fast. |
dragFree | boolean | false | Momentum dragging with no snap points. |
noDrag | boolean | false | Turn dragging off — navigate only with the controls. Recommended for vertical sliders: a vertical slider claims the up/down touch gesture, so on a phone a visitor swiping over it moves the slides instead of scrolling the page and gets stuck. With this on, the gesture goes back to the browser. |
slideClick | boolean | false | Make each slide a navigation target — clicking it (or Enter/Space when focused) scrolls to it. Ideal for a thumbnail-style slider synced to a main one: clicking a thumb moves both. A real link or button inside a slide keeps working. See Click to select. |
Auto scroll (group) | — | — | Continuous marquee-style scrolling (Embla Auto Scroll, bundled — nothing to upload). enabled, speed, direction (forward/backward), pauseHover, disableDrag (turn dragging off for a pure marquee). Best with Loop on; it overrides Autoplay. |
effect | select | "slide" | How one slide replaces the next — slide, fade, fade-up, zoom, cover, reveal, none. Anything other than slide is meant for sliders showing one slide at a time, so the option only appears when mobile, tablet and desktop are all set to 1 slide; see Slide effects. |
edgeFade | boolean | false | Fades slides in and out at the left and right edges instead of cutting them off. Useful when the slider is narrower than the screen. Off by default: the mask it needs also limits how far a slide may grow vertically — see Edge fade. |
lazy | boolean | false | Load slide images just before they scroll into view instead of all at once — see Lazy loading images. |
Autoplay — see Autoplay
| Prop | Type | Default | Notes |
|---|---|---|---|
enabled | boolean | false | Advance automatically. |
interval | string | "5" | Seconds between advances. Only shown once enabled is on. |
Connect
| Prop | Type | Default | Notes |
|---|---|---|---|
sliderId | string | "" | A name for this slider, so controls placed outside it can find it. |
syncId | string | "" | Sliders sharing the same id move together — see Synced sliders. |
There are no "show arrows / show dots" switches: a control appears because you added it, and disappears because you removed it.
FancySlide
FancySlide has no props — drop any content into its content slot
(cards, images, quotes, whatever the design needs).
Controls
| Prop | Type | Default | Notes |
|---|---|---|---|
sliderId | string | "" | Which slider to control. Leave it empty when the control sits inside the slider — it finds the one around it. |
direction | select | "next" | FancySliderArrow only — prev or next. |
label | string | "Next slide" | FancySliderArrow only — its accessible name. |
iconDirection | select | "auto" | FancySliderArrow only — which way the glyph points. auto follows the slider it drives (up/down on a vertical one, including arrows placed outside it); horizontal / vertical force it. Picking vertical explicitly also turns the text skin on its side (writing-mode), since choosing it on purpose means you want the whole control vertical — auto leaves PREV / NEXT upright, as they read the same either way. |
format | select | "fraction" | FancySliderCounter only — fraction shows 2/5, current shows just 2. |
separator | string | "/" | FancySliderCounter only — what sits between the two numbers. |
pad | boolean | false | FancySliderCounter only — pad single digits with a zero (01, 02…). |
The counter is aria-hidden: every slide already announces itself as
"2 of 5", so repeating it would only add noise for screen reader users.
States
The script keeps a set of attributes in sync as the slider moves, so a design can style any state without JavaScript:
| Attribute | Where | Meaning |
|---|---|---|
data-start / data-end | Root | The slider is at its first / last position (arrows also disabled there when loop is off). |
data-playing / data-paused | Root | Autoplay is running / temporarily paused. |
data-active | Slide | The current slide. |
data-prev / data-next | Slide | The slide immediately before / after the current one. |
data-visible | Slide | Any slide currently within the viewport. With a stacked effect only the active slide counts as visible. |
data-leaving | Slide | The slide the slider is coming from. Used by cover and reveal; also handy for exit animations. |
data-direction | Root | forward or backward — which way the slider last moved. |
data-stacked | Root | Present whenever the effect is not slide. |
For example, dim non-active slides with
.fe-slider__slide:not([data-active]) { opacity: 0.5; }.
Slides per view
slidesMobile, slidesLandscape, slidesTablet, and slidesDesktop set how
many slides show at each size, mobile-first: the mobile value is the base, and
each wider tier overrides it as space allows. Four tiers, smallest first:
| Tier | From | Prop |
|---|---|---|
| Mobile (portrait) | base, below 478px | slidesMobile |
| Landscape | 478px | slidesLandscape |
| Tablet | 767px | slidesTablet |
| Desktop | 991px | slidesDesktop |
Landscape falls back to the mobile count when it's left at its default, so a slider that only differentiates mobile/tablet/desktop behaves exactly as before.
They measure the slider's own width, not the browser window. The slider
carries its own container context (the "Has Me" selector, :has(> &)), so
the same slider set to 3 columns shows three slides in a full-width section
and drops to one inside a narrow sidebar — without any extra configuration.
The thresholds come from the shared --fe-landscape (478px), --fe-tablet
(767px) and --fe-desktop (991px) custom media tokens.
Under the hood these become CSS variables on the root, and each slide is
flex: 0 0 calc(100% / var(--fe-slider-count)) — pure CSS, with no
JavaScript re-measuring on resize.
Responsive breakpoints
By default the counts react to the slider's own width (a container query). That's the right behaviour almost everywhere: a reusable slider adapts to whatever slot it sits in.
The exception is a slider that never gets wide even on a large screen — one living inside a narrow column, like the thumbnail strip in the Synced Hero. Its own width never crosses the tablet/desktop thresholds, so container mode keeps it stuck on the mobile count (1) no matter how big the window is.
Turn on viewportBreakpoints to switch the counts to the window width
(a media query) instead. Same slidesMobile/Tablet/Desktop values, same
thresholds (767px / 991px) — only the width they measure changes:
viewportBreakpoints | Counts follow | Use it when |
|---|---|---|
false (default) | The slider's own width | The slider fills its breakpoint — most cases. |
true | The browser window width | The slider sits in a narrow/fixed column and you want it to react to the screen, not its slot. |
/* Default — container query: the slider's own inline-size. */
@container (inline-size >= 991px) {
.fe-slider:not([viewport-breakpoints="true"]) { --fe-slider-count: var(--fe-slider-count-d, 3); }
}
/* Opt-in — media query: the window width. */
@media (min-width: 991px) {
.fe-slider[viewport-breakpoints="true"] { --fe-slider-count: var(--fe-slider-count-d, 3); }
}
Controls outside the slider
Controls don't have to live inside the slider. To drive one from elsewhere on the page — a navigation bar, a fixed corner, the opposite column of a split layout — do two things:
- Give the slider a Slider ID, e.g.
hero. - Give each control the same Slider ID.
That's the whole link. A control with an empty Slider ID falls back to
"the slider I'm inside", which is why controls dropped in the controls
slot need no configuration at all.
Synced sliders
Give two or more sliders the same syncId and they move together — advance
one and the others follow to the same index, in either direction. Useful
for a thumbnail strip driving a main slider, or paired text/image
carousels. Leave syncId empty (the default) for an independent slider.
Click to select
Turn on slideClick to make each slide a navigation target: clicking it —
or pressing Enter/Space when it's focused — scrolls the slider to that slide.
It shines paired with synced sliders: use a second slider as
a thumbnail navigator (its slides are the thumbnails), give both the same
syncId, and switch slideClick on for the thumbnail one. Clicking a thumb
moves both. This is the lighter-weight alternative to the dedicated
thumbnail strip control when your thumbnails are just
the slides of another slider — as in the Synced Hero's cards.
Details:
- Accessibility — while it's on, each in-view slide becomes a
role="button"withtabindex="0"and keyboard support (Enter/Space). Off-screen slides stayinert, so only the visible ones are reachable. Turning the option back off restores the slide's original role. - Links inside a slide keep working — a click that lands on a real
<a>,<button>, or form control inside the slide does its own thing; it doesn't get hijacked into navigation. - Autoplay — clicking a slide respects "Pause on prev/next": with it on (default), selecting a slide stops autoplay.
<!-- main slider -->
<div class="fe-slider" data-fe-slider sync-id="hero">…</div>
<!-- thumbnail navigator: same sync-id, clickable slides -->
<div class="fe-slider" data-fe-slider sync-id="hero" slide-click="true">…</div>
Accessibility
Follows the WAI-ARIA Carousel Pattern:
- The root is a
role="region"witharia-roledescription="carousel"and yourlabelas its accessible name. - Each slide is a
role="group"witharia-roledescription="slide"and anaria-labellike "2 of 6", kept in sync automatically. - The track is an
aria-liveregion —politenormally, switched tooffwhile autoplay runs (autoplay changes aren't announced), back topolitewhen it stops. - If you turn autoplay on, add a
FancySliderPlayPausecontrol. The APG requires auto-rotating content to offer a visible way to stop the motion, and since controls are opt-in that one is on you. (Autoplay also pauses on hover and on keyboard focus regardless.) - Arrows become
disabledat the ends whenloopis off; dots exposearia-currenton the active page and show a keyboard focus ring. - Slides that are scrolled out of view are set
inert, so keyboard and screen reader users can't land on links or buttons inside an off-screen slide.
Reduced motion
For visitors who've asked for reduced motion (prefers-reduced-motion):
- Autoplay never starts, and continuous auto-scroll is not activated at all.
- The default
slideeffect switches to an instant cut instead of animating the track (the stacked effects were already reduced in CSS). - Toggling the preference is honoured live — no reload needed.
Autoplay & progress bar
With autoplay on, the slider advances automatically and shows a
horizontal progress bar counting down to the next advance. The bar is a plain
CSS variable — --fe-slider-progress runs 0→1 and the fill is
transform: scaleX(var(--fe-slider-progress)). The script advances it each
frame, so there are no @keyframes (nothing to paste into a stylesheet) and
pausing simply stops the loop, freezing the bar exactly where it is.
- Runs only while visible. An
IntersectionObserverstarts/stops the cycle as the slider enters/leaves the viewport. - Pauses on hover or keyboard focus — for real. The bar freezes exactly where it is and resumes from that same point, rather than restarting.
- The play/pause button stops and starts it on demand, and any drag, arrow, or dot click stops it for that page view.
- Respects
prefers-reduced-motion— autoplay never starts at all for visitors who've asked for reduced motion.
Slide effects
By default slides move sideways. With Slide effect (Behavior) they can instead swap in place — use it on sliders showing one slide at a time, such as a hero.
| Effect | What happens |
|---|---|
slide | The track moves horizontally. The default. |
fade | Slides cross-fade in place. |
fade-up | Cross-fade while the incoming slide rises into place from below (same both ways). |
zoom | Cross-fade while the incoming slide scales down from --fe-slider-zoom-from to its normal size. Set that below 1 (0.92) and it grows instead. |
cover | The new slide slides in over the one you are leaving, which stays put. Going back, the old one moves away to uncover the new one. |
reveal | Same idea, but the slide is uncovered like a curtain (clip-path) instead of moving. |
none | The slide swaps instantly, with no effect of its own — the starting point for your own animation. |
Navigation, autoplay, dots, counter, swiping and slider sync work exactly the same in every effect; only the visual transition changes.
Writing your own animation
Pick none and animate against [data-active]. A CSS animation restarts each
time the slide becomes the active one, so a hero can zoom its background while
the text arrives a little later:
.my-hero-slide {
/* The image is always slowly zooming while its slide is on screen */
& img {
transition: scale 6s linear;
scale: 1;
}
&[data-active] img {
scale: 1.12;
}
/* The text arrives after the image */
&[data-active] .my-hero-text {
animation: my-hero-in 0.8s 0.2s ease both;
}
}
/* In .my-hero-text itself with the Fancy Etch plugin,
or in Style Manager → Stylesheets without it */
@keyframes my-hero-in {
from { opacity: 0; translate: 0 1.5rem; }
to { opacity: 1; translate: 0 0; }
}
Where the @keyframes can go is explained in
Quickstart.
Each slide also carries --fe-slider-index (0, 1, 2 …), handy for staggering,
and the root carries data-direction so an animation can differ depending on
which way the slider moved.
Edge fade
When a slider doesn't span the full width of the screen, slides entering and leaving are cut off in a hard vertical line at the viewport edge. Turning on Fade edges (Behavior) dissolves them instead.
It is off by default on purpose. The fade is a mask-image, and a mask also
clips vertically: anything a slide does that grows past the viewport — a hover
lift, a scale, a drop shadow — stops being painted once it goes beyond
--fe-slider-fade-headroom. Turn it on where you want the effect and raise the
headroom to fit whatever your slides do:
.my-slider {
--fe-slider-fade: 2.5rem; /* how wide the fade is */
--fe-slider-fade-headroom: 3rem; /* room for the card lifting on hover */
}
Vertical orientation
Set Orientation to vertical (Layout) and the slider scrolls up and down
instead of left and right — drag, arrows, dots, autoplay, loop and lazy loading
all keep working on the Y axis.
A vertical track has neither a natural height nor a natural width, so both come
from props that only appear in vertical mode: Viewport height (height, e.g.
22rem or 80vh) and Max width (width, default 28rem). The width cap
matters — without it the widest slide (an image at its intrinsic size) would set
the slider's width and burst out of the layout. Slides-per-view still applies — with 1 you get one slide filling the
height, with more they stack.
<div class="fe-slider" data-fe-slider orientation="vertical"
style="--fe-slider-height: 24rem">…</div>
:::tip Vertical on mobile Turn on Disable drag (Behavior) for vertical sliders. Otherwise the slider keeps the vertical touch gesture and visitors can't scroll the page past it — they get stuck on the slider. Navigation still works with arrows, dots and thumbnails. :::
:::note Notes
Vertical applies to the default slide effect only. The stacked effects
(fade, zoom, cover, reveal, fade-up) are one-at-a-time overlays, so
picking one keeps the slider horizontal even with Orientation set to vertical.
Arrow glyphs point up/down automatically: FancySliderArrow has an Icon
direction prop set to auto, which follows the slider.
:::
RTL
The slider follows the page's writing direction — put dir="rtl" on the page (or
direction: rtl on an ancestor) and it mirrors automatically: the track scrolls
the other way, and the gap and slide order flip with it. Nothing to configure.
The stacked effects (fade, zoom, cover, reveal, fade-up) position slides
with a physical transform, so the script tags the root with data-rtl and the CSS
mirrors those offsets — cover and reveal also sweep from the other side.
Thumbnail navigation
Drop a FancySliderThumbnails next to a slider and it builds a strip of
previews — one per slide — that highlight the current slide and jump to it on
click. It's the dots control, but with pictures. No second synced slider, no
duplicated content.
The thumbnail image is found automatically, in this order:
data-thumb="…"on the slide (or on its image) — use this for a lighter preview than the full slide image;- otherwise the slide's first
<img>(itsdata-srcwhen lazy is on, else itssrc); - if the slide has no image, the thumbnail shows the slide number instead.
Skins: frame (a coloured ring on the active one, others dimmed), dim
(opacity only), bar (an underline under the active one). Thumb size, aspect
ratio, gap, radius, accent colour and inactive opacity all come from the panel.
:::note Thumbnails and lazy load
A thumbnail strip shows every slide, so it loads every thumbnail. If you also
want the main images lazy-loaded, point data-thumb at small preview files so
the strip stays light while the full images still load on demand.
:::
Lazy loading images
For sliders with many image slides, Lazy load images (Behavior) loads each image only when its slide is in view or about to be, instead of downloading them all on page load.
Give the images you want deferred a data-src (and optionally
data-srcset) instead of src, then turn the option on:
<div class="fe-slider__slide">
<img data-src="/uploads/photo-3.jpg"
data-srcset="/uploads/photo-3.jpg 1x, /uploads/photo-3@2x.jpg 2x"
alt="…" />
</div>
The script loads the in-view slides plus their immediate neighbours (so the next
one is ready), and fades each image in as it arrives — set --fe-slider-lazy-fade
to change or disable that. Images without data-src are left alone.
:::note Why not loading="lazy"?
Native lazy loading is unreliable in a carousel: slides move by CSS transform,
not by scrolling, so the browser can't tell which images are off-screen. This
option uses Embla's in-view tracking
to decide what to load instead.
:::
CSS variables
Each part owns its own variables. Select an arrow and its knobs are on the class you're looking at — you never have to go hunting in a different class. This is also what lets a control work when it sits outside the slider: it doesn't depend on inheriting anything.
.fe-slider — the slider itself:
| Variable | Default | Controls |
|---|---|---|
--fe-slider-gap | var(--space-s) | Space between slides. |
--fe-slider-ui-gap | var(--space-s) | Space between the slides and whatever is in the controls slot. |
--fe-slider-effect-duration / -ease | 0.6s / ease | Timing of the fade and zoom effects. |
--fe-slider-zoom-from | 1.08 | The scale a slide grows in from with the zoom effect. Below 1 it grows instead of shrinking. |
--fe-slider-shift | 1.5rem | How far the slide travels with the fade-up effect. |
--fe-slider-fade | 2rem | Width of the edge fade. Only applies with Fade edges on. |
--fe-slider-fade-headroom | 4rem | How far a slide may grow above or below the viewport before the fade clips it. Only applies with Fade edges on. |
.fe-slider__arrow — previous / next buttons. The Skin prop is the button's
DESIGN — solid (default), outline, minimal (glyph only), line (a
straight-arrow glyph), text (a word instead of an icon — defaults to PREV/NEXT by direction, with an optional Text field to override; ignores the corner radius). Colors (background,
icon/text, and their hover) and Size (button, glyph, radius) are their own
property groups, so an arrow is fully styled from the panel — no need to
override variables from a parent or wrapper:
| Variable | Default | Controls |
|---|---|---|
--fe-slider-arrow-size | 2.75rem | Button size. |
--fe-slider-arrow-radius | 50% | Corner radius. |
--fe-slider-arrow-color / -bg | var(--primary) / var(--base-ultra-light) | Color / background, resting. |
--fe-slider-arrow-color-hover / -bg-hover | var(--white) / var(--primary) | Color / background, hover. |
--fe-slider-arrow-glyph | chevron SVG | Arrow shape, as a mask-image — swap it for a different arrow with no markup change. The "previous" arrow reuses it, flipped. |
--fe-slider-arrow-glyph-size | 1.1rem | Glyph size inside the button. |
--fe-slider-arrow-duration / -ease | 0.3s / ease | Hover transition timing. |
.fe-slider__dots — pagination. Most of these are exposed as properties
(Skin, Colors, Size) so you rarely touch CSS:
| Property → Variable | Default | Controls |
|---|---|---|
| Skin | dots | dots (round), progress (a bar filling by position), lines (dashes), worm (active stretches into a pill). |
Colors → --fe-slider-dot-color / -color-active | var(--base-light) / var(--primary) | Color, inactive / active. |
Size → --fe-slider-dot-size | 0.6rem | Dot diameter (dots & worm). |
Size → --fe-slider-dots-thickness | 4px | Bar/line height (progress & lines). |
Size → --fe-slider-progress-dots-width | 6rem | Track width (progress). |
Size → --fe-slider-line-width | 1.5rem | Dash width (lines). |
Size → --fe-slider-worm-length | 1.8rem | Active pill length (worm). |
Size → --fe-slider-dots-gap | 0.5rem | Space between dots. |
The Size fields are conditional on the skin — you only ever see the ones
that apply (e.g. progress shows Bar height + Track width, nothing else).
Track width is a maximum, not a fixed size: the progress track holds it
when there's room but shrinks to fit a narrow controls row (mobile) instead of
forcing the row to overflow. Arrows in the same row keep their size (they don't
squash). Set Track width to 100% for a bar that always fills the row.
Power-user CSS variables (not in the panel): --fe-slider-dot-scale-active
(1.25, active dot growth in dots), --fe-slider-dot-duration /
-ease (0.3s / ease).
.fe-slider__playpause — the same knobs as the arrow, named
--fe-slider-play-* (-size, -radius, -color, -bg, -color-hover,
-bg-hover, -glyph-size, -duration, -ease), plus
--fe-slider-play-glyph and --fe-slider-pause-glyph for the two icons.
.fe-slider__progress — the autoplay bar:
| Variable | Default | Controls |
|---|---|---|
--fe-slider-progress-height | 4px | Bar thickness. |
--fe-slider-progress-radius | 999px | Corner radius. |
--fe-slider-progress-color / -bg | var(--primary) / var(--base-light) | Fill / track. |
--fe-slider-progress-duration | 5s | Set automatically from autoplayInterval — don't override by hand. |
.fe-slider__controls — the optional row: --fe-slider-controls-gap
(var(--space-s)), the space between the controls it holds.
Extending
The root element exposes a small API for custom templates:
el.feSlider.next(), .prev(), .goTo(i), .stopAutoplay(), and the raw
.embla instance. It also emits a fe-slider:select event (with the new
index in detail) on every change — wire external UI to that.