PlCarousel
A strip of slides, one of which is in view. A scroll container with snap points underneath, so swiping and dragging are the platform's own rather than a gesture handler pretending to be one.
import { PlCarousel } from 'plass-ui';
<PlCarousel label="Places">
<img src="/harbour.jpg" alt="The harbour at dawn" />
<img src="/dunes.jpg" alt="Dunes" />
</PlCarousel>;import 'package:plass_ui/plass_ui.dart';
PlCarousel(
label: 'Places',
value: slide,
aspectRatio: 16 / 7,
onChanged: (int next) => setState(() => slide = next),
children: <Widget>[HarbourPhoto(), DunesPhoto()],
);Props
| Prop | Type | Default | Description |
|---|---|---|---|
| variantshared | 'solid' | 'glass' | 'ghost' | 'glass' | What the frame is made of. Never dyed — a carousel holds other people's pictures. ghost has no frame at all |
| sizeshared | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'md' | The frame's radius, the size of the arrows and the size of the dots |
| colorshared | 'primary' | 'secondary' | 'success' | 'warning' | 'danger' | 'info' | 'primary' | Semantic colour role. Arbitrary colour values are not accepted |
| densityshared | 'default' | 'compact' | 'default' | Padding only — never the height, never the type scale |
| elevationshared | 0 | 1 | 2 | 3 | 0 | Drop shadow depth of the frame. 0 is the default and it is flat |
| value | number | — | Which slide is showing, counted from 0. Use with onValueChange |
| defaultValue | number | 0 | Which starts showing, for an uncontrolled carousel |
| onValueChange | (index: number) => void | — | Called with the slide now in view |
| loop | boolean | true | Whether the arrows wrap from the last slide back to the first. Off, they go inert at the ends |
| autoPlay | boolean | false | Advances on its own. Pauses on hover, on focus, in a background tab, and for reduced motion |
| interval | number | 5000 | How long each slide is held, in milliseconds |
| arrows | boolean | true | The previous/next buttons |
| indicators | boolean | true | The row of position dots under the frame |
| label | string | 'Carousel' | The carousel's accessible name. Never drawn |
| previousLabel | string | 'Previous slide' | The arrow's name |
| nextLabel | string | 'Next slide' | The arrow's name |
| slideLabel | (index: number, count: number) => string | — | Names one slide, and the dot that goes to it |
| Prop | Type | Default | Description |
|---|---|---|---|
| children * | List<Widget> | — | The slides. Every child becomes one |
| value * | int | — | Which slide is showing, counted from 0. Use with onValueChange |
| onChanged | ValueChanged<int>? | — | Called with the slide that should come into view. Left out, the carousel is frozen where it is |
| loop | bool | true | Whether the arrows wrap from the last slide back to the first. Off, they go inert at the ends |
| autoPlay | bool | false | Advances on its own. Pauses on hover, on focus, in a background tab, and for reduced motion |
| interval | Duration | Duration(seconds: 5) | How long each slide is held, in milliseconds |
| arrows | bool | true | The previous/next buttons |
| indicators | bool | true | The row of position dots under the frame |
| aspectRatio | double? | — | How tall the frame is, as a width-to-height ratio. A PageView has to be given a height; left out, the carousel takes what the layout hands down |
| variantshared | PlassVariant | PlassVariant.glass | What the frame is made of. Never dyed — a carousel holds other people's pictures. ghost has no frame at all |
| sizeshared | PlassSize | PlassSize.md | The frame's radius, the size of the arrows and the size of the dots |
| colorshared | PlassColor | PlassColor.primary | Semantic colour role. Arbitrary colour values are not accepted |
| densityshared | PlassDensity | PlassDensity.standard | Padding only — never the height, never the type scale |
| elevationshared | int | 0 | Drop shadow depth of the frame. 0 is the default and it is flat |
| label | String | 'Carousel' | The carousel's accessible name. Never drawn |
| previousLabel | String | 'Previous slide' | The arrow's name |
| nextLabel | String | 'Next slide' | The arrow's name |
| slideLabel | String Function(int index, int count)? | — | Names one slide, and the dot that goes to it |
Every other <div> attribute passes through to the region.
What the shared axes mean across the library is in prop conventions.
Composition
A scroll container with snap points, and everything good about this component follows from that one choice.
Swiping and two-finger dragging on a trackpad are the platform's own scrolling rather than a gesture handler imitating it, so momentum, rubber-banding and the scrollbar come with them. The strip runs the other way under RTL without being told, because scrolling is directional and a translate is not. Nothing is transformed, so the house rule against moving a surface holds here without an exception.
Slides are not a sub-component either. Every child becomes one, and the wrapper is what carries the semantics a screen reader needs, none of which a caller should have to remember to put on a photograph.
<PlCarousel><img /><img /></PlCarousel> is the whole API. The wrapper carries the snap point, the width and the role="group" / aria-roledescription="slide" pair.
Underneath it is a PageView, Flutter's own scrolling with snap points. It brings one parameter the React build has no need of, aspectRatio. A browser's strip is as tall as whatever is in it, while a PageView lays every page out at the viewport's size and so has to be given a height. Leave aspectRatio out and the carousel takes the height the layout around it hands down.
Examples
variant
The frame, on the same three materials as every other container, and never dyed. A carousel holds other people's pictures. ghost has no frame at all, which is what to use when the pictures already have edges of their own.
loop
On by default: the arrows wrap from the last slide back to the first. Turn it off and they go inert at the ends instead, which is the honest thing for a set that has a beginning and an end. A gallery of three photographs does, a rotating banner does not.
autoPlay
Off by default, and deliberately so. A carousel that moves while it is being read is the most complained-about pattern there is, and every one of the guards below exists because of a way that goes wrong.
- It pauses while the pointer is over it.
- It does not start at all for a reader who has asked for reduced motion.
- It needs somewhere to report the move to: a carousel nothing is listening to has nothing to advance, so it does not try.
- It pauses on focus anywhere inside it, which is the important one, a keyboard reader who has tabbed into a slide is reading it.
- It stops while the tab is in the background.
- The live region that announces the current slide goes silent while it is running, because a screen reader saying a new slide's name every five seconds is what makes a page unusable.
The dots
The current dot is a short bar rather than a bigger circle. It grows along the row it is in, so the row's height never changes and the dots either side of it do not move. Width and colour are the only two things that travel, which is what keeps the indicator inside the rule against scaling anything.
Every dot is a real button named after the slide it goes to, so the row is a way to navigate rather than a read-out.
Accessibility
- The carousel names itself, and every slide has a name of its own.
- The arrows and the dots are real buttons with real names.
label,previousLabel,nextLabelandslideLabeldecide what those names are.
- The whole thing is a
regionwitharia-roledescription="carousel", and every slide agroupwitharia-roledescription="slide". - No off-screen slide is hidden. A slide can hold a link or a button, and an
aria-hiddensubtree that is still in the tab order is the exact shape of the bug where a keyboard reader lands somewhere their screen reader refuses to describe. The strip is scrollable, so everything in it is genuinely reachable. - Where the reader is is announced as a sentence in a polite live region, and never while
autoPlayis on. - The strip itself is focusable and scrolls with the arrow keys, which is the browser's own key handling on a scroll container, so it is already right under RTL.
- A
PageViewbuilds the page in view and the one beside it, so an off-screen slide is not in the tree at all rather than in it and hidden. Nothing is lied about: the pages that exist are the pages a reader can reach right now, and scrolling builds the rest.
Differences from the React build
| React | Flutter | Why |
|---|---|---|
value / defaultValue / onValueChange | value / onChanged | Flutter's own controls are controlled, and so is every stateful widget in this package. |
| — | aspectRatio | A browser's strip is as tall as what is in it; a PageView lays every page out at the viewport's size, so it has to be given a height. |
| a polite live region | — | Flutter has one live region and no politeness levels. The slide's name is on the slide, which is where a screen reader reads it. |
| pauses in a background tab | — | There is no tab to be in the background of. |
| pauses on focus inside the frame | — | Focus inside a PageView does not reach the frame the way a DOM focus event does. The pointer pause is what carries it. |
className, style | — | There is no class list and no style attribute to pass through. |