GrCarousel

  • core
  • data
  • API · 20 props
  • ⌨ keys

Shows a set of slides one screen at a time — swipe, arrows, dots and thumbnails, with autoplay you can pause.

import { GrCarousel } from '@feugene/granularity/components/GrCarousel'

Overview

When to take it

  • a gallery of a product or an object — several shots where there is room for only one: the strip of thumbnails shows how many more there are and leads straight to the right one;
  • onboarding and promo screens — three to five slides in a row that are flicked through with a finger rather than chosen by name;
  • a strip of cards wider than the screen — reviews, selections, related materials: the slide here is arbitrary markup rather than a picture;
  • a screen without a viewer — a shop window or a monitor in a hall: the show runs by itself, and the pause button and prefers-reduced-motion stop it.

When to take something else

NeedTake
Named sections that are switched betweenGrTabs + GrTabPanels
Examine a picture: zoom, rotation, full screenGrImageViewer
Show everything at once, folding the excess in placeGrCollapse
A vertical list where all the rows are visibleGrList / GrDataTable
The user changes the order of the itemsGrSortableList
Stages of a process where the status of each mattersGrSteps

Variants every tone and view, rendered by the real component

tone

  • primary
  • neutral
  • success
  • warning
  • danger
  • info
  • slate
  • azure

The grid is live: hover and press it. Hover, focus and active states are not drawn here — they belong to the browser, not to a prop.

Playground controls come from the API, not from hand

Stage

Loading…

0 changed· state is in the URL

Examples 4 each with a live demo and its code

API generated from the package · cannot drift

Props

PropTypedefaultDescription
tone
"primary" | "neutral" | "success" | "warning" | "danger" | ""primary" | "neutral" | "success" | "warning" | "danger" | "info" | "slate" | "azure" | undefined
undefinedThe tone of the current switcher by the shared scale of the package.
modelValuev-modelnumber | undefinedundefinedThe index of the current frame. Not passed — the strip keeps the position itself.
ariaLabelstring | undefinedundefinedThe name of the carousel. The APG requires it: `aria-roledescription` does not count as a name.
virtualboolean | undefinedfalseDo not draw the content of the frames far from the current one. What is virtualised is **the inside of the frame, not the frame**: the frames come through the slot, and the strip cannot not render someone else’s node. An empty wrapper holds its place in the row and weighs almost nothing, while the pictures and cards leave the DOM.
orientation"horizontal" | "vertical" | undefined"horizontal"The axis the strip moves along. Vertically the viewport needs a **definite height**: the step is computed from it, and without a height the strip degenerates into a column of frames.
autoplayboolean | undefinedfalseStart the show. Under `prefers-reduced-motion` it does not start — the toggle stays.
autoplayIntervalnumber | undefined5000The pause between frames, ms.
loopboolean | undefinedtrueClose the strip into a loop. Off — the arrows at the edges go dim.
virtualOverscannumber | undefined1How many frames on either side of the current one to draw beyond it. It cannot be set below one: the next frame is obliged to be ready **before** the move, otherwise the strip would slide onto emptiness, and the content would appear once already in place.
indicators"none" | "dots" | "thumbnails" | undefinedundefinedThe kind of frame switcher. `none` also changes the role of the slides themselves.
activationMode"automatic" | "manual" | undefinedundefinedWhether an arrow over the switchers flips the frame at once or only moves the focus.
arrowsboolean | undefinedtrueThe "back/forward" arrows.
swipeboolean | undefinedtrueFlipping by dragging the pointer.
landmarkboolean | undefinedfalseMake the carousel a landmark of the page (`role="region"`).

Slots

SlotTypeDescription
defaultanyThe frames of the strip — `GrCarouselSlide`.
prev{ disabled: boolean; }An icon of your own for the "back" button. It replaces the content of the button but not the button itself: the name, `aria-disabled` and the behaviour at the edge stay with the component.
next{ disabled: boolean; }An icon of your own for the "forward" button.

Events

EventSignatureDescription
update:modelValue(event: "update:modelValue", index: number): void—

Methods

MethodsSignatureDescription
indexnumberThe current frame — the only way to read the position without `v-model`.
countnumberThe number of frames: the set lives in the slot, and there is nothing outside to count it with.
playingbooleanWhether the show is running. The pause under the cursor does not switch it off.
next() => voidThe next frame. Stops the show; without `loop` it does nothing on the last frame.
prev() => voidThe previous frame. Stops the show; without `loop` it does nothing on the first frame.
goTo(index: number) => voidGo to a frame by index; an index beyond the strip is clamped to the edge. Stops the show.
play() => voidStart the show — the same as with the toggle.
pause() => voidStop the show and announce the stop to a screen reader.

Types

Every exported type of the subpath, grouped by the file it lives in. Names, generics and bodies come from the package’s declaration files and cannot drift.

Import
import type { GrCarouselConfigurableProps, GrCarouselProps, GrCarouselEmits } from '@feugene/granularity/components/GrCarousel'

Filedist/types/components/GrCarousel/config.d.ts

grCarouselConfigconst
const grCarouselConfig: import("@feugene/granum/contract").GranumComponentDescriptor<"GrCarousel">;

Filedist/types/components/GrCarousel/defaults.d.ts

GrCarouselConfigurablePropsinterface
interface GrCarouselConfigurableProps {
    indicators: GrCarouselIndicators;
    activationMode: GrCarouselActivationMode;
    tone: GrTone;
}

Filedist/types/components/GrCarousel/GrCarousel.vue.d.ts

GrCarouselPropsinterface
interface GrCarouselProps {
    modelValue?: number;
    autoplay?: boolean;
    autoplayInterval?: number;
    loop?: boolean;
    orientation?: GrCarouselOrientation;
    virtual?: boolean;
    virtualOverscan?: number;
    indicators?: GrCarouselIndicators;
    tone?: GrTone;
    activationMode?: GrCarouselActivationMode;
    arrows?: boolean;
    swipe?: boolean;
    landmark?: boolean;
    ariaLabel?: string;
    ariaLabelledby?: string;
    prevLabel?: string;
    nextLabel?: string;
    playLabel?: string;
    pauseLabel?: string;
    indicatorsLabel?: string;
}
GrCarouselEmitsinterface
interface GrCarouselEmits {
    (e: 'update:modelValue', index: number): void;
}

Filedist/types/components/GrCarousel/GrCarouselSlide.vue.d.ts

GrCarouselSlidePropsinterface
interface GrCarouselSlideProps {
    label?: string;
    thumbnail?: string;
}

Filedist/types/components/GrCarousel/grCarouselStyles.d.ts

GrCarouselIndicatorstype
type GrCarouselIndicators = typeof GR_CAROUSEL_INDICATORS[number];
GrCarouselActivationModetype
type GrCarouselActivationMode = typeof GR_CAROUSEL_ACTIVATION_MODES[number];

Filedist/types/components/GrCarousel/safelist.d.ts

grCarouselSafelistconst
const grCarouselSafelist: string[];

Filedist/types/components/GrCarousel/index.d.ts

GrCarouselInstancetype
type GrCarouselInstance = ComponentExposed<typeof GrCarouselComponent>;

Accessibility

APG pattern

carousel

Keys

Home / End

Usage from the library prose

The ring closes without a rewind

loop is on by default, and it used to simply reset the index: the strip scrolled back through all the slides. On a ten-slide carousel with autoplay every tenth step was a long backward run.

Now at the boundary of the ring the outermost slide temporarily moves over to the opposite end, the strip travels to it as to an ordinary neighbour, and only after the transition the position is reset without animation. There is no clone here and there cannot be one: the slides arrive through a slot, and a copy of someone else’s node would take its state and its refs along with it.

The model is updated at once, without waiting for the end of the transition: modelValue is about which slide is shown, not about whether the strip has arrived.

The axis of motion
vue
<GrCarousel orientation="vertical" class="h-56">
  <GrCarouselSlide label="November">…</GrCarouselSlide>
</GrCarousel>

A vertical strip needs a definite viewport height. The step is computed from it, and without a height the column of slides grows with its content, and “by one slide” means the whole strip at once. If the height is zero, the component says so in a dev build instead of silently drawing a column.

The arrows move over to the axis of motion, the cross-axis scrolling of the page stays with the page (touch-action), and the strip of indicators stays horizontal: it is about choosing a slide, not about the direction of the strip.

Virtualisation
vue
<GrCarousel virtual :virtual-overscan="1">
  <GrCarouselSlide label="Slide">…</GrCarouselSlide>
</GrCarousel>

What is virtualised is the inside of a slide, not the slide. The slides arrive through a slot, and the strip cannot refuse to render someone else’s node — but it can ask it not to draw its content. An empty wrapper holds its place in the row and weighs almost nothing, while the pictures and cards leave the DOM.

virtualOverscan is how many slides on either side to draw beyond the current one. It is never less than one: the next slide must be ready before the transition, otherwise the strip would travel onto emptiness and the content would show up already in place. With loop the window is closed together with the strip — the left-hand neighbours of the first slide are the last ones.

The slides arrive through a slot, not through an items prop

The markup of a slide is everyone’s own: for a gallery it is an <img>, for onboarding a heading, a paragraph and a button, for a strip of reviews a card. An array of data would force either a render function or a template prop for every kind of slide, so a slide is a GrCarouselSlide with arbitrary content inside.

A consequence worth knowing about: the component learns the composition of the strip from the slides themselves, at mount time. On the server the strip of indicators comes out empty and is filled in on the next tick on the client — the first client render repeats the server one, and hydration stays intact.

The strip has no keys of its own

The strip is not focusable and is not a scroller — the shift is done by a transform, not by scrolling. The ring of the arrow keys lives at the strip of indicators: ←/→ walk the slides, Home/End go to the ends, the strip holds a single Tab stop. With activationMode="manual" an arrow moves only the focus, and the slide is confirmed by Enter or Space.

The ring of the strip is always closed, regardless of loop: loop governs the arrows and the show, while any slide can be chosen directly in any case.

The show stops by itself

A cursor over the strip, a gesture in progress and a tab in the background put the countdown on pause: the remainder is kept, and on return the strip counts it down rather than starting the interval anew. Focus that has entered inside stops the show and does not resume it by itself — otherwise the slide would drive away from under the finger while the user reads a link inside it.

The pause button is drawn whenever autoplay is set, and there is nothing to hide it with: APG forbids a moving carousel without a mechanism to stop it.

Under prefers-reduced-motion the show does not start, but the button stays and offers to launch it — “reduce motion” does not mean “take the possibility away”. The global clamp does not help here: it kills CSS, while the countdown is driven by a timer, and the strip would keep travelling for the very person who asked it not to.

A swipe does not cancel a click

A gesture begins only where there is no link, button or field under the pointer: the interactive stuff inside a slide stays interactive. Further, the gesture must exceed a threshold — the larger of 60 px and 15 % of the strip’s width — and run more horizontally than vertically, otherwise it is page scrolling, and the carousel does not intercept it. Vertical scrolling of the page stays with the browser: the strip suppresses only the horizontal.

An interrupted gesture does not turn a slide. The browser takes the pointer away from the page — a system gesture, a call, loss of the window — and the user has not completed anything at that point.

A single slide and an empty strip are states, not edge cases

With a single slide the arrows, dots and thumbnails are not drawn at all rather than drawn disabled: a disabled control reports that there is somewhere to go. An empty strip is a v-if on the consumer’s side: the consumer writes the slides, and the component will not substitute a “nothing here” text for them.

The thumbnail is taken from three places

In descending order of strength: the slide’s #thumbnail slot, then its thumbnail prop with a URL, then the slide’s number in a tile of the same size. The third option keeps the rhythm of the strip when there is no picture at all; if no slide has supplied a thumbnail with indicators="thumbnails", the component warns in dev.

The component does not look for the first <img> inside a slide: walking the markup does not work on the server, breaks on slides without pictures and hides the contract from the reader.

Custom arrow icons and the tone of the indicators

The #prev and #next slots replace the content of the button, not the button itself: the name, aria-disabled and the behaviour at the edge stay with the component. Otherwise a custom arrow would bring along the duty to repeat the whole contract — and it is most often not repeated.

The tone of the current indicator is taken from the package’s shared scale (tone), not from a colour set of its own. A one-off repaint is still done through --gr-carousel-dot-active: the prop sets the role, the hook sets the value. neutral has no saturated colour, so its current dot (and the frame of the current thumbnail) takes the colour of the main text, --gr-fg, like the neutral counter of GrBadgeWrap. The surface grey will not do here: the inactive dots are drawn with it.

The current dot carries two marks: a fill and an outline around it. A fill alone is not enough — at a diameter of eight pixels it distinguished the dot only by shade, and on a muted tone the difference almost vanished. The outline adds shape, that is, it works where colour is not told apart.

The strip of thumbnails moves itself, not the page

Scrolling to the current thumbnail is computed arithmetically and assigned to the scrollLeft of the strip itself. scrollIntoView is no good here: it moves all the ancestors, the document included — and a carousel that has gone below the fold would drag the page back to itself on every step of autoplay.

Limits
  • there is no virtualisation — all the slides lie in the DOM. A strip of hundreds of heavy slides requires a solution of its own on the consumer’s side;
  • there is no endless strip by cloning — loop rewinds the index, so on the transition from the last slide to the first the strip travels back rather than continuing its motion;
  • there is no vertical orientation;
  • the arrows lie above the slide — they are meant for a photo, where there is nothing to cover. A slide with text must keep its content away from them with margins at the edges (a 36 px button plus an 8 px gap on each side), otherwise the first letters of a line go under the button. The component cannot move the arrows outside the strip;
  • the height is set by the tallest slide — the slides lie in one row. A constant height, if one is needed, is set by the content of the slide.