PlGallery
A set of pictures, arranged. Four layouts (a contact sheet, a masonry, a justified library and a quilt) with captions, a pointer treatment and an optional lightbox on all four.
import { PlGallery } from 'plass-ui';
<PlGallery
items={[
{ src: '/harbour.jpg', alt: 'A harbour at dusk', ratio: 4 / 3 },
{ src: '/bridge.jpg', alt: 'A bridge over a river', ratio: 3 / 2 }
]}
layout="masonry"
preview
/>;import 'package:plass_ui/plass_ui.dart';
PlGallery(
items: <PlGalleryItem>[
PlGalleryItem(
image: const NetworkImage('/harbour.jpg'),
semanticLabel: 'A harbour at dusk',
ratio: 4 / 3,
),
],
layout: PlGalleryLayout.masonry,
preview: true,
);A viewer lifts itself out of the tree, so a gallery with preview on needs an Overlay above it, WidgetsApp with a navigator and MaterialApp both provide one.
The four layouts are the component: everything else (the captions, the pointer treatment, the viewer) is the same in all of them, and choosing between them is one prop rather than four components.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| items * | readonly PlGalleryItem[] | — | The pictures, in the order they are drawn |
| layout | 'grid' | 'masonry' | 'justified' | 'quilted' | 'grid' | How the tiles are arranged. Four questions rather than four looks |
| columns | PlassResponsive<number> | { xs: 2, sm: 3, lg: 4 } | How many tiles across, per breakpoint. justified decides for itself, row by row |
| gap | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | number | string | 'md' | The space between tiles — a step of the size ladder, a number in pixels, or a CSS length |
| ratio | number | string | 1 | The shape of a tile in grid, and what an item with no ratio of its own falls back to |
| rowHeight | number | 220 | How tall a row aims to be in justified, and how tall one cell is in quilted |
| rounded | boolean | true | Rounds the tiles |
| caption | 'none' | 'below' | 'overlay' | 'hover' | 'none' | Where a tile's title and description go. hover is overlay that arrives with the pointer |
| hover | 'none' | 'lift' | 'dim' | 'zoom' | 'lift' | What a tile does under the pointer. zoom moves the photograph inside a frame that stays put |
| preview | boolean | false | Opens the picture full size when a tile is chosen. The viewer is fetched on demand |
| onItemSelect | (item: PlGalleryItem, index: number) => void | — | Called when a tile is chosen, whether or not there is a viewer |
| label | string | 'Gallery' | The list's accessible name |
| itemLabel | (index: number, total: number) => string | (i, n) => `${i} of ${n}` | How a tile and the viewer counter say where in the set they are |
| empty | ReactNode | — | What is drawn when items is empty. Nothing at all by default |
| sizeshared | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'md' | Type scale and radius |
| colorshared | 'primary' | 'secondary' | 'success' | 'warning' | 'danger' | 'info' | 'primary' | Semantic colour role. It reaches the focus ring and the placeholders |
| Prop | Type | Default | Description |
|---|---|---|---|
| items * | List<PlGalleryItem> | — | The pictures, in the order they are drawn |
| layout | PlGalleryLayout | PlGalleryLayout.grid | How the tiles are arranged. Four questions rather than four looks |
| columns | PlassResponsive<int> | PlassResponsive(2, sm: 3, lg: 4) | How many tiles across, per breakpoint. justified decides for itself, row by row |
| gap | double? | the size ladder's step | The space between tiles — a step of the size ladder, a number in pixels, or a CSS length |
| ratio | double | 1 | The shape of a tile in grid, and what an item with no ratio of its own falls back to |
| rowHeight | double | 220 | How tall a row aims to be in justified, and how tall one cell is in quilted |
| rounded | bool | true | Rounds the tiles |
| caption | PlGalleryCaption | PlGalleryCaption.none | Where a tile's title and description go. hover is overlay that arrives with the pointer |
| hover | PlGalleryHover | PlGalleryHover.lift | What a tile does under the pointer. zoom moves the photograph inside a frame that stays put |
| preview | bool | false | Opens the picture full size when a tile is chosen. The viewer is fetched on demand |
| onItemSelected | void Function(PlGalleryItem, int)? | — | Called when a tile is chosen, whether or not there is a viewer |
| semanticLabel | String? | 'Gallery' | The list's accessible name |
| itemLabel | String Function(int, int)? | (i, n) => `${i} of ${n}` | How a tile and the viewer counter say where in the set they are |
| empty | Widget? | — | What is drawn when items is empty. Nothing at all by default |
| sizeshared | PlassSize | PlassSize.md | Type scale and radius |
| colorshared | PlassColor | PlassColor.primary | Semantic colour role. It reaches the focus ring and the placeholders |
PlGalleryItem
| Prop | Type | Default | Description |
|---|---|---|---|
| src * | string | — | Where the picture is |
| alt * | string | — | What the picture says. Required, for the reason PlImage requires it |
| id | string | — | A stable identity. Defaults to src |
| title | ReactNode | — | The first line of the caption |
| description | ReactNode | — | The second, one step down the scale and muted |
| full | string | — | A larger file for the viewer. Falls back to src |
| ratio | number | string | — | The picture's own proportion. masonry and justified are laid out from it, before anything has loaded |
| cols | number | 1 | How many columns the tile takes in quilted |
| rows | number | 1 | How many rows the tile takes in quilted |
| Prop | Type | Default | Description |
|---|---|---|---|
| image * | ImageProvider<Object> | — | The picture |
| semanticLabel * | String | — | What the picture says. Required, for the reason PlImage requires it |
| id | String? | — | A stable identity. Defaults to the image provider itself |
| title | String? | — | The first line of the caption |
| description | String? | — | The second, one step down the scale and muted |
| full | ImageProvider<Object>? | — | A larger file for the viewer. Falls back to src |
| ratio | double? | — | The picture's own proportion. masonry and justified are laid out from it, before anything has loaded |
| cols | int | 1 | How many columns the tile takes in quilted |
| rows | int | 1 | How many rows the tile takes in quilted |
Every native <ul> attribute passes straight through. children is excluded because the pictures are items, and onSelect because the component's is onItemSelect and reports an item rather than an event.
A className lands on the list. classNames reaches the five parts inside it: item, image, caption, title and description.
What the shared axes mean across the library is in prop conventions.
Examples
layout
grid is a contact sheet: every tile the same shape, whatever shape the files are. masonry keeps each picture's own proportion and stacks the columns. justified keeps the proportions and fills every row to the edge, the arrangement a photograph library uses, and the only one where no tile is cropped and no space is left over. quilted is a grid whose tiles may take more than one cell.
A masonry deals across before it deals down. CSS columns fills the first column top to bottom before it starts the second, so a set numbered 1 to 12 reads down the left edge and the first three pictures a reader meets are stacked on top of each other. Dealt this way the first row is items 1, 2 and 3, which is the order they were given in.
ratio
Every layout is laid out from the item's own ratio rather than from anything measured, which is why a wall of forty photographs is right in the first frame and does not reflow as the files arrive. A set without one falls back to the gallery's ratio and comes out as a grid of squares in a masonry's clothing.
{ src: '/dunes.jpg', alt: 'Dunes at first light', ratio: 2 }
{ src: '/terrace.jpg', alt: 'A stepped terrace', ratio: '2 / 3' }A number or the way CSS writes one, 2 and '2 / 3' both work, because that is how a ratio is written and this library does not make a caller translate it.
A double: width over height. There is no string form, because Dart has no CSS to be consistent with.
Two of the layouts measure here and neither does on React. CSS does a justified row with flex-grow and a quilt with grid-auto-flow: dense; there is no such thing in Flutter, so those two pack themselves inside a LayoutBuilder. The arrangement is the same; what differs is who computed it.
caption
below puts the two lines under the picture, overlay writes them across the foot of it on a wash dark enough to survive a pale photograph, and hover is overlay that arrives with the pointer.
A tile with neither a title nor a description draws no caption at all, whatever caption says. A row of pictures with one caption under it and three gaps is worse than a row with none.
hover
lift and dim are depth and colour, which is how everything else in the library answers a pointer. zoom is the one that scales, and it is the exception the design language names: what moves is a photograph inside a frame that stays exactly where it was, with no text on it to resample.
quilted
A tile takes cols columns and rows rows of the grid. The flow is dense: a tile too wide for the space left on a row does not push everything down, it drops to the next row that fits it and a later, narrower tile fills the hole.
A span wider than the grid is clamped rather than refused, which is what the caller meant by cols: 99.
preview
Opens the picture full size, with the rest of the set an arrow key away. It is not a carousel: a carousel is a set somebody is being shown in order, and this is one picture with a way to the next, so there is no autoplay, no wrap, and the arrows stop at the ends rather than looping back to a photograph the reader has already seen.
full is the larger file, when the tile is a thumbnail. A set that has only one size of each picture needs nothing.
{ src: '/thumb/harbour.jpg', full: '/full/harbour.jpg', alt: 'A harbour at dusk' }The viewer is behind a React.lazy, so a wall of thumbnails costs nothing for a lightbox nobody opened. The same bargain PlImage makes with the same prop.
Accessibility
- A real
role="list"with a name, and onerole="listitem"per picture. A masonry's lanes are list items holding lists of their own rather than<div>s between the<ul>and its<li>s, which is markup a screen reader reads as a list with nothing in it. - A tile is only a button when something happens when it is pressed. Its name is the picture's own words plus where it sits: "A harbour at dusk — 1 of 6", so a reader tabbing a wall of thumbnails is told which one of how many they are on.
itemLabelis how that sentence is written in another language, and it is a callback rather than a string with slots because the word order differs.- The viewer's arrow keys are bound on the sheet rather than on its buttons: the focus is wherever the reader last put it, and a key that only worked from one place is a key that looks broken everywhere else.
- The viewer's counter is a live region, so an arrow key says where it landed to a reader who cannot see the picture it landed on.