PlSpoiler
Content that is covered until somebody asks for it. A plot twist, an answer, a photograph nobody has agreed to look at yet.
import { PlSpoiler } from 'plass-ui';
<PlSpoiler reversible>
<p>Rosebud was the name painted on the sled he had as a child.</p>
</PlSpoiler>;import 'package:plass_ui/plass_ui.dart';
PlSpoiler(
reversible: true,
child: const Text('Rosebud was the name painted on the sled he had as a child.'),
);Props
| Prop | Type | Default | Description |
|---|---|---|---|
| revealed | boolean | — | Whether the content is uncovered. Use with onRevealedChange for a controlled one |
| defaultRevealed | boolean | false | Where an uncontrolled spoiler starts |
| onRevealedChange | (revealed: boolean) => void | — | Called when the revealed state changes |
| label | ReactNode | 'Reveal' | The reveal button's label |
| hideLabel | ReactNode | 'Hide' | The hide button's label, when reversible is on |
| description | ReactNode | false | 'This may contain spoilers' | The line above the button. false leaves a cover with nothing written on it |
| action | ReactNode | — | Replaces the default reveal button entirely. The replacement is yours to wire up |
| reversible | boolean | false | Keeps the content coverable: once revealed, a hide button appears under it |
| maxHeight | number | string | — | Clamps the covered box to this height. Revealing releases it |
| blur | number | 10 | How hard the content is blurred, in pixels |
| padded | boolean | true | Inner padding around the content |
| variantshared | 'solid' | 'glass' | 'ghost' | 'glass' | What the sheet is made of. Never dyed. ghost draws no box at all |
| sizeshared | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'md' | The sheet's radius, and the size of the button on it |
| colorshared | 'primary' | 'secondary' | 'success' | 'warning' | 'danger' | 'info' | 'primary' | Semantic colour role. Arbitrary colour values are not accepted |
| densityshared | 'default' | 'compact' | 'default' | Padding around the cover's own text and button |
| elevationshared | 0 | 1 | 2 | 3 | 0 | Drop shadow depth. 0 means no shadow at all |
| Prop | Type | Default | Description |
|---|---|---|---|
| child | Widget? | — | What is being covered |
| revealed | bool? | — | Whether the content is uncovered. Left out, the spoiler keeps its own: the one widget in the package that is happy uncontrolled |
| onRevealedChanged | ValueChanged<bool>? | — | Called when the revealed state changes |
| label | String | 'Reveal' | The reveal button's words, and its accessible name |
| hideLabel | String | 'Hide' | The hide button's label, when reversible is on |
| description | Widget? | Text('This may contain spoilers') | The line above the button. null is a cover with nothing written on it |
| action | Widget? | — | Replaces the default reveal button entirely. The replacement is yours to wire up |
| reversible | bool | false | Keeps the content coverable: once revealed, a hide button appears under it |
| maxHeight | double? | — | Clamps the covered box to this height. Revealing releases it |
| blur | double | 10 | How hard the content is blurred, in pixels |
| padded | bool | true | Inner padding around the content |
| variantshared | PlassVariant | PlassVariant.glass | What the sheet is made of. Never dyed. ghost draws no box at all |
| sizeshared | PlassSize | PlassSize.md | The sheet's radius, and the size of the button on it |
| colorshared | PlassColor | PlassColor.primary | Semantic colour role. Arbitrary colour values are not accepted |
| densityshared | PlassDensity | PlassDensity.standard | Padding around the cover's own text and button |
| elevationshared | int | 0 | Drop shadow depth. 0 means no shadow at all |
Every other <div> attribute passes through to the sheet.
What the shared axes mean across the library is in prop conventions.
The cover
The cover is a blur, and that is the whole design. A reader can see that there is something there, roughly how much of it there is, and (with maxHeight) that it has been clamped. What they cannot do is read it by accident, which is the one thing a spoiler is for.
Blur alone is not cover, though. It takes a paragraph apart but leaves its colour and its rhythm, and a photograph blurred at 10px is still recognisably a photograph of a face, so a wash of the page's own surface goes over the top. That settles two things at once: the content goes to a wash of its own colours, and the button gets something to stand on rather than floating over whatever happened to be underneath it.
A short spoiler is as tall as its own cover, not as tall as its content: the two share one cell, so a one-line spoiler does not clip the button it is asking somebody to press.
Height on reveal
Covering and uncovering moves nothing on the page around it. Both of the things that could move it are held instead of removed: the cover keeps its cell, and the reversible hide row keeps its own. Neither is taken out of the layout on the way in and put back on the way out.
The cover is the one that matters most, because it is usually the taller of the two. A cover is a line of explanation and a button, so against a single line of covered text it is what holds the sheet open. Drop it when the spoiler is revealed and the sheet collapses to the line, taking everything below it up the page with it. Held in place it still measures, and nothing moves.
What is held is not reachable. Both are inert while they are hidden, so neither is tabbable, readable by a screen reader, nor selectable: a reader who has already uncovered the content is never offered a reveal button they cannot see.
maxHeight is the exception, as above.
Examples
variant
The three materials, read as a container's: the sheet is never dyed. What a spoiler holds is a photograph, a paragraph, a plot twist, and it arrives with its own colours. The family shows up on the button and in the hairline and stops there.
ghost draws no box at all, which is what a spoiler sitting inside running prose usually wants.
maxHeight
Left out, the box is exactly as tall as what it holds, the right default for a paragraph or a picture. Set it for something long enough that a page of blurred content would be a page of nothing.
The clamp is only ever on the covered state. Revealing something and leaving it in a box with a scrollbar is answering the wrong question.
This makes maxHeight the one exception to the rule below: it is the only thing that changes the sheet's height between the two states, and it is meant to.
reversible
Off by default: once it is uncovered, it stays uncovered. Turn it on and a hide button appears under the content, which is what a page full of them wants, a reader who revealed the wrong one can put it back.
The row is reserved from the start rather than added when the spoiler opens, and held invisible under the cover until it is needed. A control that arrives with a reveal is a button's worth of height that grows the sheet on the way in and shrinks it back on the way out, which moves the page twice around the thing somebody is pressing. The empty row is never seen, because the cover is drawn over it.
padded and media
Turn the padding off for something that should reach the edges. A covered image is the case this component is most often used, and the blur is doing real work there: the shape and the colours are visible, the subject is not.
Accessibility
- While it is covered the content is out of the focus order and off the accessibility tree. A spoiler somebody can tab into is not a spoiler.
descriptionis read before the button, which is what tells somebody why they are being asked. Turning it off leaves a cover that says nothing, worth doing only where the surrounding page already has.
- All of that is one attribute:
inert, which also takes the content out of the selection. A spoiler that could be defeated by Ctrl+A is not a spoiler. - The reveal button reports the state it controls and points at the content it uncovers, so a screen reader announces it as the disclosure it is.
ExcludeSemantics,ExcludeFocusandIgnorePointerare the three widgets that say what that one attribute says. Text selection needs no third: Flutter's is opt-in, so a covered paragraph is only selectable if the screen wrapped it in aSelectionArea, and one that did should not have.
Differences from the React build
| React | Flutter | Why |
|---|---|---|
revealed / defaultRevealed / onRevealedChange | revealed / onRevealedChanged | The one widget in the package that is happy uncontrolled, because what is remembered is a thing the reader did to this box rather than a value the screen owns. Leave revealed out and it keeps its own. |
label, hideLabel as nodes | label, hideLabel as String | They are the button's words and its accessible name, and only a string can be both. |
description: ReactNode | false | description: Widget? | Dart already has a word for "not set". |
maxHeight: number | string | maxHeight: double | Pixels stay pixels. There is no CSS length to accept. |
inert | ExcludeSemantics + ExcludeFocus + IgnorePointer | The same three things, said as the three widgets that do them. |
className, style | — | There is no class list and no style attribute to pass through. |