plass_ui 1.5.0 copy "plass_ui: ^1.5.0" to clipboard
plass_ui: ^1.5.0 copied to clipboard

A Flutter UI component library made of glass and gradients — tinted surfaces, coloured shadows, and light that follows the pointer. Accessible, themeable, dark mode built in.

Changelog #

This package's history. The React package keeps its own at packages/react/CHANGELOG.md, because the two version independently.

vNext (2026--) #

1.5.0 - 2026-09-19 #

Breaking changes #

  • PlHighlight's query takes a List<String> rather than a list of patterns. A RegExp inside a list was escaped and searched for as the characters it is written with, so query: [RegExp(r'\d+')] looked for the literal text \d+ and marked nothing. A list is literal text now and says so: it is what a search box is wired to, and a reader typing 1 + 1 is looking for 1 + 1. Pass a pattern on its own instead, which is what such a list would have been anyway — RegExp(r'error|\d+') says what ['error', RegExp(r'\d+')] was reaching for. The React query has always been string | string[] | RegExp, and the two now take the same shape.

Added #

  • A labelled field can put its label in its own top edge, with labelPlacement: PlassFieldLabelPlacement.notch. top is still the default, so nothing moves without being asked. A notch buys back a row of vertical space and ties the name to the box rather than to whatever is above it, and the cut is a real one: the edge is painted with the label's segment clipped out of it, because a Plass field is translucent and the page behind it belongs to the app — there is nothing to paint over the line with. It is on every field-shaped widget, which is PlTextField, PlSelect, PlCombobox, PlNumberField, PlFilePicker, and the six that share a picker's trigger: PlDatePicker, PlDateRangePicker, PlDateTimePicker, PlTimePicker, PlColorPicker and PlTreeSelect. PlassTheme's defaults carry it as well, for a product whose forms all notch. Two things follow from the label sitting on the edge, and both are deliberate: focus thickens the edge rather than drawing a ring, because a ring is a rectangle and would run straight through the label; and the label is painted over the edge rather than laid out above it, so it no longer widens the control and a long one wants fullWidth. PlOtpField is the one labelled field that does not take it: a notch is a segment out of one continuous edge, and a row of separate boxes with gaps between them has none, so its label stays above the row. PlSwitch's own labelPlacement is a different axis — which side of a tick the label sits on — and is unchanged.

  • A page can make the library's glass read the backdrop once, with a BackdropGroup. Each sheet ran its own σ22 read of what was behind it, so a list of sixty cards paid for sixty. Every sheet now asks the nearest BackdropGroup for a backdrop key, and Flutter shares one read between the filters that carry it — wrap the list and the reads collapse into one. Nothing changes for a page that adds no group, because a sheet with none above it takes the same null key it had. Where the group goes is the app's to say, since two sheets that overlap must not share one; a modal's barrier and a tour's dimming cover the screen, so the library keeps those two out of every group itself.

  • PlassTokens.copyWith and PlassColorFamily.copyWith are public, so an app can bring its own colours. PlassTheme.tokens took a set, and the only sets that could be built were PlassTokens.light() and PlassTokens.dark() — a brand colour, a quieter glass or a chart palette of your own had nowhere to go. Start from the set you are changing, replace what is yours, and hand the result over; withFamily is the short form for one family. Both types now compare by value, so a set rebuilt with the same values does not renotify the subtree under it. radius, duration and ease stay constants the library reads directly and are still the same in every theme.

Fixed #

  • The interaction light is on the field shells as well as on the keys. PlassGlowLayer was built for every interactive surface and reached four of them — PlButton, PlToggle, PlPill and PlChip — so a form of Plass fields answered a pointer with nothing while the button under it bloomed. It is now on PlTextField, PlNumberField, PlSelect, PlCombobox, a PlFilePicker's drop zone, each segment of a PlSegmentedButton, and the six triggers that share a picker's shell: PlDatePicker, PlDateRangePicker, PlDateTimePicker, PlTimePicker, PlColorPicker and PlTreeSelect. PlassTokens.fieldGlow and fieldFlash are the two colours for it, the family's own tint in all three variants rather than switching on the variant as glow and flash do — a solid field is the well, which is the glass at its most opaque, and white light on it would be white light on a near-white sheet. A segment takes whichever pair matches what it is standing on: the tile if it is chosen, the trough if it is not. The light goes out while a control is disabled or read-only, and it follows a finger on a touch screen the way it always has, because the press layer is what carries it there.

    Three are deliberately without it. A PlCheckbox, a PlRadio and a PlSwitch's track are the size of the text beside them, and a bloom of the light's radius inside a 20px box is a flat wash that slides about rather than light. A PlOtpField's slots are separate boxes with gaps between them, so there is no one surface for a light to cross.

  • A PlFilePicker says why it turned files away. onRejected was the only way to hear about it, so a picker an app had not wired reported a refusal by doing nothing at all. The box now says it under itself, one line per reason and counted, grouped by reason rather than named one file at a time — a folder handed to a picker with a maxFiles of five is ninety-five lines of the same sentence, and what the limit is stays in hint. showRejections: false turns it off for an app that says the same thing somewhere of its own, and onRejected fires either way. It is a live region rather than an announcement, and it does not mark the field invalid: what was rejected never reached the value. filesRejectedType, filesRejectedSize and filesRejectedCount join PlassLabels in all seven languages, which takes it to ninety-nine.

  • A PlFilePicker spreads a halo while a file is over it. The wash and the ring-coloured edge dragging already drew are both inside the box, and a reader dropping a file is looking at the file under their cursor rather than at the box — so the state was there and easy to miss. A soft halo of the family now spreads outside the edge, which is how a PlSlider's thumb marks being dragged. The box still neither grows nor lifts. The React build marks the same state the same way, and until now it marked nothing at all.

  • A PlGallery in a scroll view asks for a picture only when the reader is near it. Every tile resolved its picture the moment the board was built, so a gallery of sixty photographs asked for sixty decodes before one of them was on screen — the React build has never had to, because a browser defers an <img loading="lazy"> on its own. A tile now waits until it is within a screen of the nearest Scrollable above the gallery, holding the same stand-in the picture draws while it loads, in the same box, so nothing moves when the picture arrives. A gallery with no scroll view over it is built whole, as before, and a picture that has been asked for is kept when the reader scrolls back past it.

  • PlassChartSeries.dashed draws a dashed line. The field was documented and listed in the props table, and the painter never read it — a caller marking a forecast got a solid line and nothing to say it was a forecast. The line is now cut into a 6px dash with a 4px gap, and the React build takes the same prop. Only a line has a line to dash: it does nothing on a bar, or on a stacked band, whose fill is the mark.

  • A line, bar or area chart hands a screen reader every number in it. The text it carried was one value per series, the last, so a reader could hear where a line ended and never what it did on the way — while the scatter and the heatmap in the same package already read every value. It is now each visible series, then the categories it has a value at and what it was worth there: "Revenue: Jan 12; Feb 19; Mar 15. Cost: Jan 8; Feb 11; Mar 9". A gap is left out rather than read as a category with nothing after it, and a chart given no categories leaves the positions out too, because the order of the reading already carries them. semanticValue still replaces the whole line.

  • A stacked-to-full bar or area chart writes the value behind each share the way the rest of the chart writes a number. The two charts kept their own copy of the renormalising and their own copy of the number writer, so a tooltip said 48300 where the axis beside it said 48.3K. Both now call one stackToFull, which is what the React build already did, and the label is written compactly unless a format says otherwise.

  • A time axis of minutes or seconds before 1970 starts at its own data. The first tick was worked out with ~/, which truncates towards zero, so a negative timestamp floored to the unit above it: the axis began a minute after the first point and one minute later than the React axis. It is floored towards negative infinity now, as Math.floor does on the web.

  • A treemap of more than thirty-two tiles colours tiles of equal value as the React build does. The tiles are sorted by area, and Dart reaches for a quicksort past thirty-two items, so two tiles of the same value could swap places and take each other's colour. Ties now keep the order they were given, which is what a JavaScript sort does for nothing.

  • A PlScatterChart mark's ring is as thin as the React build's. The surface showing through around each mark was stroked at twice markGap, and a stroke straddles the path with the fill over it keeping only the outer half, so the ring came out 2px where the web draws 1.

  • A chart writes its numbers the way the React build writes them. With no format of its own an axis, a tooltip and the summary wrote 48300 where the web wrote 48.3K, and 1.50 where the web wrote 1.5. From ten thousand up a number is now compact, 12.3K, 1.2M, 1.5B, 1.5T, with a value that rounds up to a thousand moving a unit along, and under it the plain number with at most two decimals and no trailing zeros. A number is still not grouped, because the package ships no intl and a separator is the locale's to choose; pass a format for one.

  • A time axis whose two ends are the same moment opens a day around it. min and max naming one instant left the axis with no width, and the two builds failed differently: every mark landed on the origin here and a screen off the plot on the web. The guard that already opened a day around a single instant in the data now covers a caller's own bounds too.

  • A cross chart marker is one outline. It was two overlapping rectangles, each outlined on its own, so the ring around the mark drew a cross through the middle of it. It is now the same twelve-cornered outline the React build draws.

  • A PlAnimateMarquee with a speed of zero or less stands still. The travel was divided by the speed, so zero gave an infinite number of milliseconds: rounding that threw, and the widget did not build at all. A speed of zero or less is now read as not moving and holds the strip where it is, as paused does, and an explicit duration still decides on its own.

  • A minimized PlWindowPane keeps its body. Rolling a window up took the content out of the tree, so everything it held — a half-filled form, a scroll position, a chosen tab — was gone when the window came back down. The body now stays in the tree, off stage and out of the focus order, which is what the window-pane page says of both builds and what the React build's inert body already did.

  • A PlStepper panel is named after the step it belongs to. Neither the panel under a horizontal rail nor the one inside a vertical step carried a name, so a screen reader landing in one was told nothing about which step it was for. Both are now a node named by the step's label, which is what the stepper page has always said and what the React panel's aria-labelledby does. A step whose label is not text is left as it was.

  • A PlFloatingBottomNavigation with no onChanged still marks the current destination. Leaving the callback out counted every destination as unavailable, so the key went quiet and the glyph on it turned muted: a bar the app drives from elsewhere showed nothing as current. The key stays lit and the current glyph keeps its ink now, and no disc takes a press, as before. It is the rule PlBottomNavigation already follows.

  • An unavailable destination in a PlBottomNavigation is dimmed. A PlBottomNavigationItem with disabled was drawn in the muted ink a resting destination already has, so nothing told the two apart. It is now dimmed and drained of colour, which is the opacity-50 saturate-[0.35] of the React item and what PlFloatingBottomNavigation already did with its own.

  • A PlHotKeys shortcut reads its keys in order wherever it sits. Each cap carried its own name, so a cap with a spoken name was a node of its own while a plain letter was loose text that merged in ahead of it: inside a PlButton or a PlCommandPalette row, ⌘N read "N, Command". The shortcut is now one node named by its keys in order, "Command N", and the caps are off the semantics tree.

  • A press on a PlColorPicker track sets the value it landed on. The square and the two rails read a press against the box including their hairline border, while the thumb is placed across the box inside it, so a press near either end set a value up to a pixel away — about a degree of hue. Both are read against the same box now.

  • An empty semanticLabel on a PlImage is no description at all. semanticLabel: '' left the picture on the semantics tree with an empty name, and named a preview button "— preview". Empty now says what alt="" says in the React build: the picture is decorative and off the tree, and a preview is named by the word for a preview on its own.

  • The typing dots of a PlChatBubble run the React build's cycle. The three dots lit in turn over 1.2 seconds here and over 1.25 on the web, so the same thread shown in the two builds drifted apart as it was watched. Both take 1.25 seconds now, and the slowed cycle under a reduced-motion preference is unchanged.

1.4.0 - 2026-09-15 #

Breaking changes #

  • PlPagination.pageLabel, PlRating.valueLabel, PlFilePicker.removeLabel and PlCombobox.removeLabel are nullable. Left out, each one now reads the theme's label pack, so code that reads one of these fields and calls it needs a null check. PlRating.defaultValueLabel still returns the English sentence.

  • Without rowKey, a PlDataTable row is keyed by its position in rows, and every callback's index is that position. The default key and the index handed to rowKey, cell, onRowPressed and isRowSelectable used to be the row's place in what was drawn, while onSelectedChanged looked the rows up by their place in rows. Ticking the first row on page two also showed the first row on page one as ticked and handed back rows[0], and a sort or a search did the same. Both now count from the top of rows, so a row keeps its key and its index wherever a sort, a search or a page puts it. Code that used index as a place on the screen, such as numbering the rows as drawn, has to count them itself. A selection kept by default keys now names different rows, and rowKey is still the way to key rows that survive rows changing.

  • A Spacer or an Expanded in a Column directly inside a PlGrid cell throws. A row now lays each cell out at its own height before stretching it, so on that first pass the Column has no height for the flexible child to fill. It used to work because the row asked every cell for an intrinsic height, which is also what made a cell holding a LayoutBuilder throw. To pin a footer to the bottom of a stretched cell, give the Column MainAxisAlignment.spaceBetween instead.

  • Picking a day in PlDateTimePicker keeps the moment inside minDate and maxDate. The day that holds a bound stays selectable, and picking it used to keep the clock as it was, or put midnight on it, without checking the bound again. With minDate: DateTime.now(), picking today reported today at 00:00. The clock is now moved into the bounds, up to the next whole minute (or second, with showSeconds) at or after minDate and down to the last one at or before maxDate. A clock that is already inside the bounds is kept as before.

Added #

  • PlTimelineItem takes a key. Steps were matched by position, so inserting a step at the start of a PlTimeline handed each stateful child the state of the step that had been in its place, and a key on the child could not help. A step is now matched by its item's key, which is what a React element's key does. Without one, steps are matched by position as before.

  • PlCard takes a headingLevel. The title's depth in the screen's outline, 1 to 6, which is what title={<h2>…</h2>} gives a card in the React build. Left out, the title is not a heading, as before. A pressable card is a button whose title is its name, so the level is not applied to one.

  • Ten words join the label packs, for the strings that were written into a widget in English with no way to translate them but a parameter: acknowledge (the OK on PlConfirmProvider's alert), optional (for PlStep.optional, which takes a widget), sidebarOpen (PlSidebarTrigger), spoilerWarning (PlSpoiler), filePickerTitle (PlFilePicker), and the five words a PlChatBubble's mark says, messageSending, messageSent, messageDelivered, messageRead and messageFailed. All seven packs translate them. PlConfirmProvider.acknowledgeLabel is now nullable and falls back to the pack.

  • label on PlModal and PlDrawer, the name a screen reader announces the layer with when it opens. title is a widget and has no text to hand over, so both layers used to open with no name at all. It is the same parameter PlOverlay and PlCommandPalette already have.

  • PlAspectFit.scaleDown, for PlAspectRatio and PlImage. It is contain that never enlarges something smaller than the box.

  • PlImage takes a width and a height. Together they are the file's own pixel size, and the box keeps their proportion before the picture arrives, turned for a picture on its side, as the React build's <img> does. One alone sizes the box on that axis: a lone height is a box that tall across the width it is given, taking its width from a ratio when there is one, and a lone width is a box that wide, never wider than the space it has. A narrowed box sits at the start of its space, and preview's press target and focus ring stay on the box.

  • PlImage takes a position. An Alignment, which decides which part of the picture a cover crop keeps and where the other fits leave their empty space. It is read on the picture as it is shown, so Alignment.topCenter keeps the top of what the reader sees through rotate and flip, with the same arithmetic as the React build. It is typed Alignment rather than AlignmentGeometry, so it stays on the same side of the photograph under a right-to-left direction.

  • PlImage takes a letterbox. It fills the part of the box that contain, none and scaleDown leave empty. PlImageLetterbox.blur draws the picture itself behind it, covering the box and blurred, turned, mirrored, placed and treated like the picture, from the same ImageProvider and so from the same cache entry. It is off the semantics tree and takes no pointer. PlImageLetterbox(decoration) paints any Decoration behind the picture instead.

  • PlImagePlaceholder. Given as a PlImage's placeholder, it stands a small copy of the picture in while the file arrives, from any ImageProvider. It is drawn under the picture with the picture's fit, position, rotate, flip and treatment, stays until the picture has finished fading in over it, and is then removed in one step. blur blurs it by that many logical pixels. The skeleton is not drawn while a picture stands in, a stand-in whose own file does not load draws nothing and reports nothing, and the stand-in needs a reserved box, as the skeleton does.

  • PlImage takes a rotate. 0, 90, 180 or 270 degrees clockwise, and any other number goes to the nearest quarter, rounded the same way as in the React build. The picture is turned with a RotatedBox, so a picture on its side is laid out on its side: a ratio is kept as the shape of the layout, and without one the widget takes the turned shape of the picture. The placeholder, the fallback and the watermark stay upright, and preview opens the picture turned the same way.

  • PlImage takes a flip. PlImageFlip.horizontal, vertical or both, along the axes the picture is shown on, so a horizontal mirror swaps left and right on the screen whether or not rotate has turned it. preview opens the picture mirrored the same way.

  • PlGallery carries the new PlImage parameters to its pictures. A PlGalleryItem takes rotate, flip, position and a PlImagePlaceholder, and the gallery takes fit (still PlAspectFit.cover by default) and letterbox for every tile. An item's ratio stays the stored file's proportion: a picture on its side is dealt into a masonry lane, packed into a justified row and opened in the viewer by its turned proportion, while a grid tile keeps the gallery's shape.

  • PlWindowPane takes a resizable. Eight handles just inside the frame, the same eight the React build has: the four edges, the four corners, and the same sums behind them, so a window dragged by its left edge moves as it widens rather than growing out of its right one. minWidth and minHeight are the floor it stops at, onResize reports the size, and a drag on a leading edge reports through onOffsetChanged as well. resizeLabel names the one handle a keyboard can reach — the bottom-right corner, which the arrow keys move sixteen pixels at a time — and the other seven are kept off the semantics tree. The handles go away while the window is maximized or rolled up. resizeWindow had been in PlassLabels and in all six locale bundles since the widget shipped, waiting for this.

  • PlImage takes a filter. Six named treatments — grayscale, sepia, saturate, desaturate, contrast and dim — resolved to the same amounts the React build writes into its CSS chain, so a sepia is one colour across the two packages. colorFilter is the escape hatch and takes any ColorFilter of your own, in place of a CSS chain that would mean nothing here. internal/css.dart gains sepiaFilter and contrastFilter alongside the brightness and saturation matrices it already had. The treatment goes on the picture and not on the skeleton or the fallback.

  • PlImage takes a watermark. PlImageWatermark says what the mark reads, where it goes, how visible it is and at what angle; PlImageWatermarkPlacement.tile covers the whole picture instead of a corner. A tiled mark is one CustomPainter for however many copies the box holds rather than a widget each, and the canvas is turned once with the grid laid out on the turned canvas, so the repeat has no seam. It is drawn only once the picture has arrived, is excluded from the semantics tree, takes no pointer, and follows the picture into preview.

  • PlTabs answers the wheel. A bar with more tabs than room is a scroll view like any other, and a horizontal one reads the horizontal half of a scroll while a mouse only ever produces the vertical one — so the bar under the pointer sat still while whatever was behind it moved. A vertical wheel over an overflowing bar now moves it along, exactly as it does over a PlScrollZone, and wheel: false turns it off. internal/wheel.dart is the shared half of the two.

  • PlScrollZone and PlTabs take an overscroll. PlassOverscroll.contain, the default, or PlassOverscroll.auto. See Changed for what the default alters.

Fixed #

  • A PlTabs bar with more tabs than room opens with the chosen tab in view. A bar whose value was a tab past the end of the strip opened scrolled to the start, so on a narrow screen nothing showed which tab was open. The strip now moves just far enough to show the chosen tab as the bar is first laid out, without animating and without scrolling anything around it.

  • A PlBottomNavigation with no onChanged still marks the current destination. Leaving the callback out took the wash and the colour off the current item along with the press, so a bar the app drives from elsewhere showed no destination as current. The current one now keeps its wash and its colour, and no item takes a press, as before.

  • A PlCommandPalette row says its name once. Each row carried its label as a semantics label on top of the text that drew it, so a screen reader read "Open" as "Open Open" and "Copy" as "Copy Copy Put it on the clipboard". A row is now one node named by what it draws, the label and then the description and the keys of the shortcut, as in the React build. A disabled row is still announced as a disabled button.

  • A closing PlAccordion section keeps its body until the panel has closed. The body vanished the moment value stopped holding the section, and only the empty space it left shrank. It now stays on screen, cut off by the shrinking panel, as it is revealed while the panel opens, and it cannot take the focus or be read out while it closes.

  • Each PlAccordion header is a heading. A screen reader's heading navigation passed over the sections, since nothing marked a header as one. Each header row, with its action, is now inside a heading of level 3, as the React build's <h3> is, and the header is still announced as a button.

  • A disabled PlCalendar is announced as disabled. Its days and its header buttons could not be pressed, but a screen reader still heard each of them as an enabled button. While disabled is set or there is no onChanged, each is now announced as disabled, with no action. The calendar looks the same.

  • A labelled PlImage preview says that it opens a preview. Its press target was named by semanticLabel alone, so a screen reader heard what the picture is but not what pressing it does. It is now named by the label and then previewLabel in lower case, or the label pack's preview when that is left out, "A portrait — preview", as the React build names its button after alt.

  • The typing dots of a PlChatBubble keep moving when animations are disabled. With disableAnimations on, the dots stopped, and three dots that hold still do not say that somebody is typing. They now light in turn on a slower cycle, 2.6 seconds instead of 1.2, as in the React build.

  • A PlColorPicker thumb sits on its value down the square and the rails. Each thumb was placed from the height of its track including the hairline border, so its centre sat a pixel below its value on a rail and up to 2 pixels below it at the foot of the square.

  • A readOnly PlRadioGroup is no longer announced as disabled. The set keeps its focus stop, but its semantics and those of each option said they were not enabled. The set and its options are now enabled and read only, the options still have no tap action, and the React build's aria-readonly on the group says the same.

  • A readOnly PlSwitch is no longer announced as disabled. It keeps its place in the focus order, but its semantics said it was not enabled. It is now enabled and read only, and still has no tap action, as the React build's aria-readonly says.

  • A PlColorPicker thumb sits on its value at every size. Each thumb was placed as if it were the md size, so at xs, sm, lg and xl its centre was up to 2 pixels beside the colour it showed, on the square and on the rails.

  • A PlColorPicker swatch the picker cannot read is left out. A swatch such as 'red' was drawn as a clear circle that did nothing when pressed, since named colours are not read. It is now not drawn, and a list with nothing readable in it draws no swatches.

  • A disabled PlTextField, PlNumberField or PlOtpField leaves the focus order. Tab stopped on it and drew a focus ring, although nothing could be typed into it. It is now passed by Tab, as a disabled PlButton is and as in the React build. A read-only field still takes the focus.

  • A readOnly PlCheckbox is no longer announced as disabled. It keeps its place in the focus order, but its semantics said it was not enabled. It is now enabled and read only, and still has no tap action, as the React build's aria-readonly says.

  • A PlCalendar with no onChanged is disabled. Its dartdoc said a calendar nothing listens to is inert, but only disabled took it out of reach, so its days and its month buttons still took a Tab and a press. It is now greyed out and out of the focus order, as disabled makes it and as every other control without a callback is.

  • The two select-all ticks of a PlTransfer have different names. Both were read as "Select all", so a screen reader could not tell which list a tick belonged to. Each is now named by selectAllLabel and then by its list's heading, "Select all Available".

  • End on a full PlRating keeps the full score. With clearable on, which is the default, End at the top score counted as choosing that score again and cleared the rating to 0, and Home on an empty row called onChanged with the 0 it already had. Home and End now go to the end of the row, and call onChanged only when the score moves.

  • A pressable PlChip draws one focus ring while its × has the focus. The chip kept its own ring while the focus was on the × inside it, so two rings were drawn at once. The chip's ring now shows only while the chip itself has the focus, as the picker trigger's does.

  • A PlImage preview with no semanticLabel has a name. Its press target was named only by semanticLabel, so without one a screen reader found a button with no name. It is now named by previewLabel, or by the label pack's preview when that is left out, as the React build names its button when alt is empty.

  • The × on a picker trigger can be reached and pressed from the keyboard. It answered only a pointer, so a value could not be cleared without one, and a PlColorPicker, which has no Clear in its footer, could not be emptied at all. It is now a focus stop after the trigger that Enter or Space presses, with its own focus ring, resting at 70% opacity until it is hovered or focused, like the × on a chip and as in the React build. The trigger's ring now shows only while the trigger itself has the focus. This covers PlDatePicker, PlDateRangePicker, PlDateTimePicker, PlTimePicker, PlColorPicker and PlTreeSelect.

  • A PlTree lets go of the focus node of a row removed from items. Each row's node was kept until the tree itself was disposed, along with the row list its key handler held, so a tree whose items were replaced over a long session kept every node it had drawn. The node is now disposed after the frame the row is removed in.

  • → on an open PlTree branch with nothing to step into keeps the focus where it is. A branch with children: [], or whose children are all disabled, handed the focus to the next row, which is a sibling. The focus now moves only into a child, as the ARIA tree pattern says.

  • A screen reader no longer reads the clock in a PlMockup system bar. The bars and the cut-out are now left out of the semantics tree, as the rest of the device already was and as in the React build, so what is announced is what the caller put on the screen.

  • A PlImage tells a screen reader what its preview shows, and reads its label once when the picture fails. The picture in the open preview was left out of the semantics tree, so only the overlay's name was heard, and it now carries semanticLabel. An image with no semanticLabel and no preview is now left off the tree entirely: its image flag merged into the widget around it, so a PlButton holding one was announced as an image. The default fallback no longer repeats the label.

  • The segment of an indeterminate PlProgressLinear enters from before the start of the groove and leaves past its end. It travelled only between the two ends, so it started and stopped flush against them and jumped from the end back to the start on every cycle. It now runs the same path as in the React build.

  • Switching disabled on while a PlTooltip is open no longer makes the rest of its PlTooltipProvider group open at once. The tooltip closed but stayed counted as open, so every other tooltip in the group skipped its delay from then on, and a wait already running could still open the disabled one out of sight. A tooltip now counts as open only while its plate is up, and switching disabled on calls off the wait.

  • A PlStat change that rounds to 0 is drawn as no movement. change: -0.04 was written -0% and still drawn with a ▼ in the danger colour. It is now 0%, muted and with no arrow, as change: 0 already was.

  • The PlConfirmProvider dialog keeps what it said while it fades out. Answering cleared the question at once, so for the 260ms of the fade the title and the body were gone and the buttons fell back to the defaults, which gave an alert a Cancel button. The sheet now fades out as it was.

  • PlTour moves to its target at once when the reader has asked for less motion. It always scrolled each target into view over 260ms, so with disableAnimations on the screen still slid past on every step. It now jumps there and measures the light once the jump is laid out, as PlBackTop already jumps.

  • The lowest bar of a PlSparkline bar strip above zero draws inside its box. A value at the foot of the scale is drawn as a bar one pixel tall, and when every value is above zero the foot is the bottom edge, so that pixel sat just below the box. It now grows up from the bottom edge, and strips with values below zero draw as before.

  • A PlCodeBlock highlightLines range stops at the last line, and a number too long for an int no longer throws. A range was walked from end to end whatever the block held, so '1-100000000' built a hundred million line numbers, and '2-99999999999999999999' made int.parse throw while the block was building. Only the lines the block has are walked now, and a number too long for an int counts as past the last line, as in the React build. parseLineSpec takes the bounds as the optional first and last.

  • A PlSparkline with shape: PlSparklineShape.bar whose values are all below zero draws inside its box. The bars grew from zero, which for such a strip lies above its top, so they covered the content above it. They now hang from the top of the strip, and strips with positive or mixed values draw as before.

  • A PlTimelineChart span that lies wholly outside a fixed min and max can no longer be reached. It was not drawn but stayed a mark, so pointing beside the plot showed its tooltip outside it. It is now left out of the marks and the summary a screen reader hears, and a span that crosses an edge answers only on the part that is drawn. The tooltip still gives that span's real start and end.

  • A bar chart with valueLabels: PlassChartValueLabels.last labels the last value it has. The label went on the last slot, so a series that ends in a gap, such as [10, 20, null], had no label at all. It now goes on the 20, as it already did on a line chart, and extremes no longer walks the whole series again for every bar on each paint.

  • The × on a PlChip, on a PlCombobox chip and on a picker trigger can be pressed anywhere in a 24-pixel square around it. It could be pressed only on the glyph, about 15 logical pixels across, which is under the minimum target size of WCAG 2.5.8. A press in the square is now handed to the ×, and nothing is laid out differently. Where the square reaches over the label or the trigger, a press inside the square goes to the × and the rest still reaches the chip or opens the picker; on the smallest chips the square moves inward to stay inside the chip. This covers PlDatePicker, PlDateRangePicker, PlDateTimePicker, PlTimePicker, PlColorPicker and PlTreeSelect.

  • A PlHeatmapChart thins its column names by one stride. Each name set its own stride from its own width, so the short names either side of a long one were written over it. The stride is now taken once, from the widest name, so every name that is written clears its neighbours.

  • A PlScatterChart reads a bubble's z, and stops reading a series switched off in the legend. The text a screen reader is handed wrote each point as x, y alone, although the docs say the z follows in brackets. It also read every series that had not started hidden, so one switched off in the legend was still read out. It now writes 1, 1 (100) as the readout does, and reads only the series that are on.

  • A tiled PlImage watermark covers every corner of a wide or tall picture. The turned grid was laid out a fixed share past each side, so from about 2:1 two opposite corners were left as empty triangles. It is now laid out from the diagonal of the picture, which no turn can uncover.

  • A pressable PlGallery tile is described by the caption it draws. The tile's semantics replaced everything under it with the picture's semanticLabel and its place in the set, so the title and description on it were never read. The lines a tile draws are now its semantics hint, a PlGalleryCaption.hover caption included before the pointer has shown it. The label is unchanged.

  • A PlCodeBlock tells a screen reader whether raw is pressed and whether the copy worked. The raw toggle had no toggled state, and the copy button changed its own word without saying it, so a reader whose focus had moved on heard nothing. The toggle now carries a toggled state, and the block announces copiedLabel or copyFailedLabel once the clipboard has answered, as the React build does with aria-pressed and its live region.

  • A PlCodeBlock is named after its language before codeLabel, as in the React build. With language: 'dart' and codeLabel: 'Code', a screen reader heard the code called "Code" rather than "dart". The language now comes first, then codeLabel, then the label pack's word for code, and a blank language counts as none.

  • A chart legend asked for on the left or the right stays on that side under RTL. PlassSide is a physical direction, but the legend was placed by the order of a flex row, which follows the writing direction, so side: 'left' put the legend on the right of an Arabic or Hebrew page. The row is now laid out physically, and the words inside the legend still read in the page's own direction.

  • Every scatter marker shape covers the same area, and a bar takes the same share of its band as in the React build. A triangle was drawn at 83% of a circle's area and a cross at 112%, so in a bubble chart the same z looked a different size for each shape, and a bar filled 0.68 of its slot against React's 0.62. Both tables now match the React ones.

  • Pointing at the legend entry of a series that is switched off no longer fades the whole chart. The bar, scatter and pie marks checked only whether the hovered entry was some other series, so hovering a hidden one dimmed every series that was actually drawn, to highlight one that was not there. All of them now ask the one condition the line chart already used.

  • PlassChartTooltipMode.item shows the one series the pointer is nearest, and none silences a PlPieChart too. item drew the whole column, which is the tooltip column already gives, and a pie drew its readout whatever the mode said, since only hidden stopped it. A chart with marks in columns now narrows the card to the series whose mark is closest to the pointer along the value axis, as the React item does, and a pie draws no readout under none.

  • PlassLabels and PlDateNames compare by their words. Neither had an ==, so a pack written where the documentation recommends writing it, labels: ko.copyWith(start: '체크인') inside build, was a new object on every frame, PlassTheme's updateShouldNotify answered true to it, and every widget under the theme rebuilt whenever the widget above it did. Two sets carrying the same words are now equal. The sentences that hold a value are functions and are compared like everything else, so a pack's own match and a closure written inline at a call site counts as new.

  • A paused PlAnimate* holds what is left of its delay. Pausing called the wait before the first pass off, and letting go started the pass at once, so an effect held 100ms into a one-second delay skipped the other 900ms. What is left of the wait is now what is waited out, as the React build does.

  • A PlTransfer row that leaves items and comes back is no longer still ticked. The ticks were kept for the life of the widget. Every read of them narrows to the rows first, so an abandoned tick drew nothing and counted for nothing — until its value came back, when the row came back ticked and its arrow came back pressable. The ticks of values that have left items are now dropped as they go.

  • Removing a PlAnimateTyping with a caret under reduced motion no longer throws. With MediaQuery.disableAnimations on, the caret never built its blink controller, so dispose built it for the first time and asked for a ticker on an element that was leaving the tree. That threw in debug builds every time the widget went away with the default caret: true. The controller is now built with the caret, and it is not started while the caret stays still.

  • PlAnimateSplit and PlAnimateScramble keep an emoji whole. Both cut their text into UTF-16 code units, so 'Ship it 🚀' cut by character left the rocket as two broken glyphs after the entrance ended, and a scramble drew halves of it as noise. Text is now cut by grapheme, so an emoji, a flag, a letter with its accent and a Devanagari conjunct each stay one character.

  • PlAnimateSplit with PlAnimateSplitBy.character wraps a line between words. Every character was a child of one Wrap, so a narrow line broke partway through a word. The characters of each word are now laid out in a Wrap of their own, so the line wraps between words and a word wider than the whole line still wraps. Chinese, Japanese, Thai and the other scripts written without spaces still wrap between their characters.

  • PlassAnimateTrigger.visible waits until the widget is on screen, not only inside the nearest scrollable. It watched the nearest Scrollable alone, so a PlAnimateCounter in a row that scrolls sideways near the bottom of a long page counted as visible along the row and started on the first frame, while it was still below the screen. It now watches every scrollable above the widget and starts once enough of it is inside all of their viewports and on the screen, which is what the React build's IntersectionObserver measures. A widget moved under other scrollables follows the new ones.

  • PlAnimateHeadline keeps turning inside a parent that rebuilds often. Every rebuild of the parent restarted the timer, so inside a parent that rebuilt every second the 2600ms interval was reset before it ever fired and the line never changed. The timer now restarts only when the line, the count, index, loop, interval, delay or the running state changes.

  • A hover effect on content that takes no focus adds no stop to the tab order. PlassAnimateTrigger.hover wrapped the child in a FocusableActionDetector, which takes the focus by default, so Tab stopped on every PlAnimate* with a hover trigger, and a screen reader found a focusable node with no name, such as one around a picture. Its callbacks also only fired while focus highlights were in keyboard mode, so on a touch device focus moved in by a tap did not start the effect. The effect now starts from a MouseRegion and from the focus landing on something inside it, as in the React build, and the widget takes no focus and adds nothing to the semantics tree.

  • When animations are turned off, the PlAnimateMarquee items past the edge of the box can be reached. The strip stood still, but the box still clipped it, so with ten headlines in a 400-pixel box only the first one or two could ever be seen. Only the first copy is now laid down, in a box that scrolls along it and is a tab stop while there is anything to scroll, so the arrow keys, Page Up, Page Down, Home and End move it. A vertical marquee scrolls down its own axis.

  • Tab passes over the copies of a PlAnimateMarquee after the first. The copies were behind ExcludeSemantics, which takes them out of the semantics tree but not out of the focus order, so a strip of ten focusable chips was thirty Tab stops. The copies after the first are now behind ExcludeFocus as well, and only the first copy's children take the focus.

  • PlRating writes a fractional score the way the pack's language writes a number. The packs interpolated the raw num, so a half star read 2.5 von 5 in German, French and Spanish, and an average such as 7 / 3 read out seventeen digits. Each pack now writes the score with its language's decimal separator and at most three decimals, 2,5 von 5, without an intl dependency, and a whole score reads 3 even when it arrives as 3.0.

  • A PlChip delete affordance is named in the word order of the label pack. The name was the pack's remove followed by the chip's Text, English's order in every language, so a Korean screen read 삭제 Design. It now comes from the pack's removeItem, Design 삭제 in Korean. A chip whose child is not a Text is still named by remove alone, and deleteLabel still replaces the whole name.

  • PlGallery says where a picture sits in the language of the label pack. A tile's name and the viewer's counter were built from an English template inside the widget, so a Korean screen read A harbour — 2 of 4. They now come from the new PlassLabels.galleryItem field, translated in all seven packs in each language's own word order, and itemLabel still wins.

  • The PlWindowPane minimize, maximize and close buttons can be reached and pressed from the keyboard. They answered only a pointer and took no focus, so Tab went past them and a keyboard could not close, roll up or maximize a window. Each button is now a stop in the tab order, Enter and Space press it, and a keyboard that reaches it draws the focus ring inside the button. A traffic light shows its mark while it has the focus, as it does under the pointer. A screen reader can now press each button too, which before was named but carried no tap action.

  • Every PlAlert is announced when it appears, whatever its severity. Only a warning or danger alert was a live region, so an info or success alert such as "Saved" that appeared after the screen loaded was never read out. Every alert is now a live region. One already on the screen when it loads is read where it stands, as with any live region. Flutter's live region has one politeness level, and it is polite, so a danger alert is read when the reader pauses rather than interrupting.

  • Each PlCarousel dot is a 24px press target. A dot answered a press only on the 4 to 8 pixels it drew, which falls short of WCAG 2.5.8 and made jumping to a slide on a phone hard. Each dot is now a 24px target with the dot drawn in its middle, and the targets sit side by side, so the dots are spaced further apart.

  • PlCarousel autoPlay keeps advancing inside a parent that rebuilds often. Every rebuild of the widget restarted its timer, so inside a parent that rebuilt every second a five-second interval never completed and the carousel stood still. The timer now restarts only when autoPlay, interval, value, the number of slides or whether there is an onChanged changes.

  • Pressing a PlTransfer arrow keeps the focus and says what moved. The pressed arrow was disabled by the move, so the focus left the widget and nothing said how many rows had gone across. The focus now goes to the first row that arrived, or to the target list when a controlled owner refused the rows, and the count is announced at polite priority from the new transferMoved label field. The new movedLabel parameter changes the sentence for one pair.

  • Accessibility sentences with a value in them are read in the language of the label pack. PlPagination's page names, PlRating's scores, PlCarousel's slide names, the remove and add rows of PlFilePicker and PlCombobox, and PlHowToSteps' step positions were English templates inside each widget. They now come from new PlassLabels fields (paginationPage, ratingValue, ratingNone, carouselSlide, removeItem, addCustom, howToStep), translated in all seven packs in each language's own word order, and the packs stay const. A widget's own parameter still wins.

  • A PlSpoiler no longer rebuilds what it covers from scratch when it is revealed. The child was wrapped in blur, clip and exclusion widgets only while it was covered, so revealing changed the shape of the tree and Flutter discarded the child's State, losing a scroll position, a playing video or typed text. The wrappers are now built in both states and only switched, and the child keeps its State across a reveal and back.

  • Pressing Reveal or Hide on a PlSpoiler from the keyboard no longer sends the focus out of the spoiler. The pressed button was put under ExcludeFocus by the rebuild that acted on the press, so its scope handed the focus back to whatever held it before, or to nothing. Revealing now moves the focus to the uncovered content, and hiding moves it back to the Reveal button. The focus is left alone when it was elsewhere on the screen.

  • The PlSlider run ends under the centre of its thumb. The run was measured along the whole rail while a thumb travels along the rail less its own size, so the run's end sat under the thumb's centre only at the middle value: below the middle it stopped short of the centre, and above it it ran past. A range's run now starts and ends under the centres of its two thumbs, and a single value's run still starts at the start of the rail.

  • A fullWidth picker no longer lays out its width samples. PlDatePicker, PlDateRangePicker, PlDateTimePicker, PlTimePicker and PlColorPicker built every sample under the value, invisible, even when fullWidth made the field's width its container's. A fullWidth field now builds only what it shows, as PlSelect already does. A field without fullWidth keeps them.

  • A press on an open PlPill's details or on its endIcon no longer calls onPressed. The whole pill answered a press, so a pill whose press opens its details folded them away as soon as someone touched the text inside, and a trailing slot that was not a control pressed the pill as well. Only the row answers a press now, as the React build's button does. Hover and the light still cover the whole pill.

  • The PlPill button says whether its details are open. With details and an onPressed, the button reported no expanded state to a screen reader. It now reports whether the panel is expanded, and a pill without details reports no expanded state at all.

  • A PlCard with interactive and no onPressed lifts under the pointer. Only a pressable card tracked the hover, so interactive on its own drew the card at rest whatever the pointer did, although the documentation said it gave the same lift as onPressed. It now follows a mouse over the card, raising the sheet and adding a level of elevation, and still adds no focus stop, role or key handling.

  • A PlCard no longer builds its content again when the pointer moves on or off it. The lift was a Transform.translate put around the sheet only while it was raised, and the focus ring a CustomPaint put around it only while it showed, so the tree above the content changed shape as the hover started and ended: an entry animation or a picture fading in inside the card played again on every hover, and the shadow's easing was cut off. Both are now always in the tree, and only the offset and the painter change.

  • A PlCollapsible with keepMounted keeps what its panel holds through a close. The panel was wrapped to take it out of the focus order and off the semantics tree only while it was closed, so the wrapper came and went with the fold and the content under it was built again each time: a field in a kept panel lost what was typed into it on every close, which is what keepMounted is there to prevent. The wrapper is now always there and only switched on and off.

  • The calendar header's month and year buttons are named by the words they show. Each button's semanticLabel was merged ahead of its text, so a screen reader heard "Choose a month, July" and the month on screen came second. The drawn month or year is now the name, and "Choose a month" or "Choose a year" follows as a hint, which is what the React build already does.

  • Every PlToast is announced, including one with the default low priority. Only a high toast was a live region, so a default toast such as "Saved" appeared and left without a screen reader saying anything. Every toast is now a live region. Flutter's live region has one politeness level, and it is polite, so a high toast is read when the reader pauses rather than interrupting; the alert and status roles that would carry the difference cannot be set on a live region.

  • A PlNavigationMenuItem destination can mark the screen the reader is on. Items had no selected, so the row could not say which destination was current. selected now marks a destination selected for a screen reader and draws its word in the accent.

  • A PlMenuSubmenu opens towards the end of the line under RTL, and its chevron turns with it. The submenu always opened to the right of its row, while under a right-to-left Directionality the arrow key that opens it is the left one, and the chevron kept pointing right. It now opens to the left under RTL, as PlNavigationMenu already did, and the chevron points that way.

  • Pressing a PlAnchor row stops offset above the heading, and jumps when animations are turned off. The press scrolled the heading to the very top of the view, under a fixed header that offset was there to clear, and always moved over the slow duration, even with the platform's animations turned off. It now stops at the same line the tracking reads, and jumps there when MediaQuery.disableAnimations is set.

  • PlScrollArea and PlScrollZone can be scrolled from the keyboard. Neither took the focus, so a box of text with nothing focusable in it, or a strip with buttons: none, could not be reached with Tab or moved with a key, although the documentation said it could. While there is somewhere to scroll, each is now a tab stop that draws the focus ring, and the arrow keys move it by a line, following the writing direction along a row, Page Up and Page Down by a screen, and Home and End to its ends. A box whose content fits adds nothing to the tab order.

  • The PlTransfer arrows point at their own lists under RTL. The selected list is at the end of the row, which is the left under a right-to-left Directionality, but the arrow to it was always drawn pointing right and the arrow back pointing left. Both now turn over with the direction.

  • A fullWidth PlSelect no longer builds every option label in its trigger. The trigger held itself open at the width of its longest label by laying out all of them, unpainted, even when fullWidth made its width the container's, so a label with a picture in it loaded that picture for every option. A fullWidth trigger now builds only what it shows. A trigger without fullWidth still lays out every label, because that is what keeps it from changing width with its value.

  • A PlSelect or picker trigger is named by the field's label. Without a semanticLabel, the trigger had no name of its own: a PlSelect took the chosen option's words as its name as well as its value, so it read "Seoul, Seoul" with "City" nowhere, and PlDatePicker, PlDateRangePicker, PlDateTimePicker, PlTimePicker, PlTreeSelect and PlColorPicker read only the value. When label is a Text, its words now name the trigger, the label is not read a second time on its own, and what is chosen stays the value. A label built from other widgets still needs semanticLabel.

  • The PlSegmentedButton tile follows the set when it changes size. The tile was measured onto the chosen segment only when the set was built, so a window resize, a turned device or a new text size left it on the segment's old rectangle. The set now measures it again whenever its size changes.

  • Pressing a PlSlider thumb keeps its value. A thumb is placed along the rail less its own size, but a press was read along the whole rail, so pressing the centre of a thumb at 0 moved it to about 3, and one at 100 to about 97. A press is now read along the same travel the thumb is placed on, in either writing direction and vertically.

  • PlTextField, PlNumberField and PlCombobox take typing as soon as they take the focus. The focus ring was added around the field when the focus arrived, which built the editor inside it again, and the new editor had no connection to the keyboard: the first keys typed after the field was pressed or tabbed to could be lost, and a PlCombobox.multiple lost the connection again when its first chip appeared. The field now keeps the same editor through both.

  • The PlAnimateTyping caret grows with the reader's text size once. The caret sits in the paragraph as a widget, and the paragraph already scales a widget by the text size, but the caret's own text applied the scale again. At 200% the caret was drawn at twice the size of the words beside it. The caret's text now leaves the scaling to the paragraph.

  • PlPagination marks the page it is on. The current page was drawn filled and read as "Page 4" like every other page, so a screen reader had no way to say which one was current, although the documentation said the name was enough. Its button is now marked selected.

  • A held PlNumberField stepper settles once, when it is let go, and stops at the end of the range. Every repeat of a held stepper called onCommitted, every 60 milliseconds, and the repeats went on after the value had reached min or max, so a caller that saved on onCommitted sent one request after another. A repeat now calls onChanged alone, the repeats stop at the end of the range, and letting go calls onCommitted once with the value it reached, without adding a step of its own.

  • A PlNumberField shows the value its parent holds after the parent turns one down. A settled or stepped number was written into the box before the parent answered, so a parent that did not take it, as in value: 5, onChanged: (_) {}, left 40 in the box while the value stayed 5. Once the parent has rebuilt, a box that disagrees with value now goes back to it.

  • A PlNumberField is named by its label. The field's node kept every descendant as a separate node, so a screen reader met a nameless text field, then the label as a line of text, then a second text field for the editor, with each stepper wrapped in an extra node. The label, the description and the error now name the field, as they do on PlTextField, and the steppers sit directly under it.

  • A PlNumberField with allowWheelScrub keeps the wheel to itself. A wheel turned over the focused field stepped the value and scrolled the page under it too, which carried the field away from the pointer, and a sideways wheel stepped the value up. The field now claims an up or down turn so nothing behind it scrolls, and leaves a sideways one alone.

  • An extended PlFloatingActionButton says its label once. The words drawn on the key were given to it as a semantic label as well, and the two were merged, so a screen reader announced "New project, New project". The drawn words are now the whole name.

  • A PlForm checks every change after a submit, as onSubmit says. The default validationMode never validated after the first submit either, so a field that failed kept its message while the reader corrected it, until they pressed submit again. From the first submit on, each change to a field is checked, and a corrected field loses its message at once.

  • Enter in a PlCombobox takes the lit row and keeps the field focused. The editor gave the focus up on Enter, which closed the list, so a multiple combobox took one pick and made the reader click back into the field for the next. And after Escape had closed the list, Enter still took the row that had been lit, which was no longer on screen. Enter now leaves the focus in the field, and takes nothing while the list is closed.

  • A PlColorPicker swatch can be reached and chosen from the keyboard. A swatch only answered a tap, so Tab went past every one of them and a keyboard reader could pick a colour only with the square and the rails. Each swatch is now a focus stop that draws the focus ring and is chosen with Enter or Space, as a button is.

  • An inline PlColorPicker is grouped under its label. The square and the rails are named "Saturation and brightness", "Hue" and "Opacity" whatever field they belong to, and the label, the description and the error were separate nodes beside them, so two inline pickers on one screen were two sets of the same sliders and an error was never tied to them. The picker is now one node named by its label, description and error with the square and the rails inside it, and an error marks them invalid.

  • The hue and opacity rails of PlColorPicker answer ↑, ↓, Home and End. A rail took only ← and →, so a keyboard reader who pressed the other keys every slider answers to changed nothing. ↑ and ↓ now move a rail as → and ← do, and Home and End take it to its two ends.

  • A PlFilePicker button says which field it belongs to. Its name was only the words inside the box, so a résumé picker and a cover letter picker on one screen were both read out as the same line. The name now starts with label and goes on with the box's own words.

  • A screen reader follows the keyboard focus across a calendar. A day, month or year cell left its focus out of the semantics tree, so moving with the arrow keys in PlCalendar and every picker moved the ring while a screen reader's cursor stayed on the cell it was on, and nothing was announced. The focused cell is now marked focused, and a screen reader can move the input focus to a cell as well.

  • The screen behind an open popup keeps working. A PlPopover, a PlMenu, a PlSelect or PlCombobox list, a picker, and a PlNavigationMenu panel closed on a press outside by covering the whole screen with a layer that took the press. The first press on a button behind the popup only closed the popup, and a drag on a list behind it did not scroll. A press outside now closes the popup and still reaches what it landed on, so a PlMenubar word pressed while another menu is open opens its own menu. A press on the popup's own trigger still only closes it.

  • The next question in a PlConfirmProvider queue places the focus again. The sheet stays open between two queued questions and reused its buttons, so autofocus did not run for the second one and the focus stayed on the button that had just been pressed. After a first question answered with its confirm button, a second, destructive question opened with the focus on its confirm button too, and pressing Enter twice approved both. Each question now gets new buttons, and its own initialFocus decides where the focus lands.

  • A toast keeps its clock stopped while it is being used. Only a mouse resting on the stack paused the timeout, so a toast could leave while the keyboard focus was on its action, while a finger was held on it, or while the app was in the background, and a reader who came back to the app had missed it. The focus anywhere in the stack, a pointer pressed on it, and the app leaving the foreground now stop the clock as hovering does, and it starts over once all of them have let go.

  • The loading toast of showFuture stays up for as long as the future takes. Its timer was stopped once when it was shown, but the timers are started again whenever another toast arrives or the pointer leaves the stack, so a request slower than the timeout lost its loading toast, and the success or failure toast that was meant to replace it never appeared. The loading toast now has no timeout at all.

  • PlTour says the next step when the focus stays on Next, and its heading names only itself. Pressing Next changed the card around the button without a word to a screen reader. The step's title is now a live region, or its content on a step with no title. The heading also took the content and the counter into its name, so the first step was announced as one heading reading "Narrow the list, Type here to filter, 1 / 3"; the title and the content are now nodes of their own.

  • An open PlTour takes the keyboard focus, so Escape closes it. Escape was bound on the card, but the focus stayed on whatever had opened the tour, so the key never reached it. The card now takes the focus when the tour opens and hands it back when the tour closes, unless the reader has moved it elsewhere in the meantime. A tour whose parent closed it by setting open to false also no longer throws: the layer was taken down in the middle of the build that closed it.

  • Moving the pointer or the focus along a PlTable or a PlDataTable no longer lays the grid out again. The band under the hovered row and the ring round the focused one were row decorations, and changing one built the whole grid again, which measures every column from every cell. Crossing from one cell to the next inside a row did it too, so a few hundred hoverable rows dropped frames under a moving mouse. The bands are now painted behind the grid, and a move repaints them without building a cell.

  • A PlTypography heading carries its level. h1 to h6 were each marked as a heading with no level, so on the web every one was announced as the same kind of heading and a screen reader could not tell a section from the one inside it. Each now carries its level, 1 for h1 through 6 for h6.

  • Each PlChip delete affordance is named after its chip. Every one was named "Remove", so moving along a row of tags read the same word for each one and never said which tag it would remove. When child is a Text, the name is now the label pack's word followed by the chip's text, such as "Remove design". A chip holding any other widget keeps the word alone, and a deleteLabel is still the whole name.

  • The picture open in the PlGallery viewer has a name. The large picture was drawn with no semanticLabel, so a screen reader announced an unnamed image. It now carries the item's semanticLabel, as its tile does.

  • A PlDataTable sort keeps rows that compare the same in the order they came in. Dart's List.sort does not keep ties in order past a few dozen items, so sorting a long table by a column of statuses shuffled the rows inside each status, and the order differed from the React table's. Ties now keep their arrival order.

  • A PlDataTable whose parent holds the sort stays unsorted when the parent clears it. A sort of null was read as the table keeping its own sort, and the table still had the one from the first press, so the third press on a heading, or a parent resetting the sort, brought an ascending sort back. The table now follows the parent's last answer.

  • A PlDataTable that keeps its own page calls onPageChanged. The table moved to the new page before comparing it with the page it was on, so the two were always the same and the callback never ran unless page was passed as well.

  • A PlDataTable sorted descending keeps its blank cells last. The built-in order put a blank value after every other value, and the direction was then applied to that answer as well, so turning a column round put the blanks first, against what the page says. The direction now turns only the values that are there. A column's own compare is still turned round as a whole.

  • A PlHighlight mark grows with the reader's text size once. A mark is a widget inside the paragraph, and the paragraph already scales such a widget by the text size, but the mark's own text applied the scale again. At 200% the marked words were drawn at twice the size of the words around them and broke the line. The mark's text now leaves the scaling to the paragraph.

  • A PlCodeBlock with a long run of spaces in its code no longer stalls. The trailing whitespace was cut with a regular expression that retries from every space in a run followed by more text, on every build and every copy, so 20,000 spaces in a pasted log took about five seconds each time. It is now cut in one pass.

  • A PlTimelineChart whose hours run over more than one day writes the date with each time. An axis stepping in hours, minutes or seconds wrote only the time, so on a range of two days 09:00 could be either day, and the tooltip and the summary a screen reader reads wrote 09:00 – 17:00 with no day either. When the axis crosses midnight, every tick, the tooltip and the summary now write the date in front of the time. A range inside one day is unchanged.

  • A PlScatterChart whose x values are dates ticks its x axis like a calendar, and xAxis.format writes its ticks. The axis ran on the dates' milliseconds with the steps a count gets and wrote each tick as a thirteen-digit number, and xAxis.format was not applied to it at all. It now takes the steps and the labels of a time axis, as PlTimelineChart does, and xAxis.format writes the ticks of any value-scaled x axis.

  • An area is filled between its two edges. PlAreaChart and a PlSparkline with shape: PlSparklineShape.area built the top and the floor of each band as two separate outlines, and two open outlines fill only the slivers between each edge and a straight line across its ends, so most of the band was left empty. The floor also went back in straight lines under a smooth or step curve, which left a gap or a double-painted strip between stacked bands. A band is now one outline, its floor drawn with the same curve as its top, as in the React build.

  • A chart stacked to full tells a reader the caller's numbers. The tooltip and the summary of PlBarChart and PlAreaChart wrote the share each part was drawn at through the caller's format, so a currency format read New $20 for a part worth 4,000. They now read a point's label, as the React build does, and a chart stacked to full sets that label to the caller's number in the caller's format.

  • A flat series under an axis pinned at one end gets the same ticks as in the React build. The axis already started at the pinned min, but its step was chosen from a band opened on both sides of the data, so a row of zeros with min: 0 was ticked every 0.5 where the React build ticks it every 0.2. Only the free end opens now, in both builds.

  • empty on a line, bar, area, scatter or timeline chart is drawn as given. Those charts read the words out of a plain Text and ignored any other widget, so a Text.rich threw a null check error and an icon over a line was replaced by the default words. The widget is now drawn as it is, in the same muted type, as PlPieChart and PlHeatmapChart already drew theirs, and those two now give a plain Text that type as well.

  • A chart legend beside the plot is a list. With PlChartLegend(side: PlassSide.left) or right, the legend of PlLineChart, PlBarChart, PlAreaChart, PlScatterChart and PlPieChart was a row with no width to wrap at, so four or five series stood in one line, squeezed the plot to nothing and overflowed. It is now a column of entries, as in the React build, no wider than two fifths of the chart, and a name longer than that wraps.

  • A chart's tooltip stays inside the chart. The card was placed by the point it described and nothing else, and a chart clips what it draws, so near the bottom of a short chart or either end of a narrow one the value was cut off. The card of PlLineChart, PlBarChart, PlAreaChart, PlScatterChart, PlTimelineChart and PlHeatmapChart is now measured, moved to the other side of the point when its side has no room, and held inside the chart.

  • A chart writes a date category as a day. A PlassChartCategory.date reached the axis, the tooltip, the pie's legend and the heatmap's columns and summary through toString, so a tooltip was headed 2026-03-01T00:00:00.000. They now write the short month and the day, Mar 1, in the names of the theme's PlDateNames, as the React build does.

  • A chart no longer rebuilds for every pixel the pointer moves. PlLineChart, PlBarChart, PlAreaChart, PlScatterChart and PlTimelineChart stored the pointer in their state, so each move rebuilt and repainted the whole chart and wrote its summary out again, even inside one column or with no mark nearby. The pointer now only moves the tooltip, the chart is rebuilt when the active column or mark changes, and the summary is written again only when the data or a series' visibility does.

  • PlCombobox's list is as wide as its field. It took the whole width of the screen, so beside a field that did not start at the left edge it ran off the right one. It now drops exactly as wide as the field, as the React build's does.

  • A chart's grid is the faintest line on the plot in the dark theme too. The dark tokens drew the grid, the axis and the baseline in the muted ink at 50%, 70% and 50%, far stronger than the React build's white at 8.4% and 12% and the muted ink at 35%, so the grid competed with the data. They now follow the stylesheet's derivation, and a package test reads styles.css to keep the two in step.

  • PlConfirmProvider, PlPopconfirm and PlCommandPalette say the label pack's words. Their Cancel and Confirm buttons and the palette's placeholder were written in English whatever the theme's labels said. confirmLabel, cancelLabel and placeholder are now nullable and fall back to the pack's confirm, cancel and commandPalettePlaceholder.

  • The row the arrow keys move to stays in view in PlSelect, PlCombobox and PlCommandPalette. Each list had a scroll controller that nothing used, so on a list taller than its popup the highlight went out of sight, and in a command palette Enter ran a command nobody could see. The list now scrolls just far enough to show the row, and only the list: the page behind the popup stays put.

  • PlModal, PlDrawer and PlToastProvider keep clear of the system's bars and a soft keyboard. On a phone, a modal's actions sat under the keyboard, a full-screen modal's and a side drawer's header and × sat under the status bar, a top toast sat in the notch and a bottom one on the home indicator. A modal and a toast stack now stand off the bars and the keyboard, and a full-screen modal and a drawer run under the bars with their content kept out from under them.

  • Escape closes a floating layer. PlPopover, PlPopconfirm, the pickers' popups, PlTooltip and PlHoverCard ignored it, and inside a PlModal it closed the modal instead of the popover opened in it, without PlPopconfirm's onCancel. Escape now closes the nearest open layer first, and cancels a popconfirm. PlTooltip and PlHoverCard also keep the focus and the pointer apart, so a pointer brushing past no longer closes what the keyboard opened, and a tooltip stays up while the pointer moves from its trigger onto the plate.

  • The left and right arrow keys follow the writing direction in PlTabs, PlRadioGroup and PlSegmentedButton, and only those two turn round in PlRating. Under RTL the three sets moved to the next choice on the right arrow, which is the one to the left of the chosen one on screen. PlRating turned every arrow round, so the up arrow lowered the score. The left and right arrows now move towards the choice they point at, and up and down mean the same in both directions, as they do in the React build.

  • A PlWindowPane whose offset is fed back moves with the pointer. The window added the distance it had been dragged to offset, and reported the same sum, so a caller passing onOffsetChanged's value back into offset, as the documentation's own demo does, moved it twice as far as the pointer went. Given onOffsetChanged, the window is now drawn at offset alone and a drag, of the bar or of a leading edge, reports where it should go. Without it, the window moves itself as before. A caller that only listened to onOffsetChanged has to pass the value back for the window to move.

  • A PlGrid cell can hold anything that measures itself with a LayoutBuilder. Every row was wrapped in an IntrinsicHeight, which asks each cell for an intrinsic height that a LayoutBuilder cannot give, so a PlGrid inside a cell threw, and so did a chart, a PlTable, a PlSlider, a PlBadge or any other widget holding one. In a release build the row came out the wrong height instead. A row now lays each cell out at its own height and then stretches the cells to the tallest. See Breaking changes for the one pattern this stops.

  • A dragged PlSidebar keeps its width when the page rebuilds. With its size coming from the theme, every rebuild of the widget above it read as a size change and put the width back to the default, so an onResize that called setState stopped the drag from moving at all.

  • A PlSlider can be adjusted with a screen reader. It was announced as a slider but had no increase or decrease action, so the swipe VoiceOver and TalkBack adjust a slider with did nothing, and a range slider's two ends were one node that neither could be moved from. Each end now answers the adjust actions by one step, and a value is read in the decimals its step has rather than rounded to a whole number.

  • Typing into a PlColorPicker's value field keeps the focus. The field was given a new focus node on every rebuild, and every keystroke that changed the colour rebuilt it, so the field lost focus and the keyboard closed after one character. The node that was thrown away was never disposed either.

  • The time columns of PlTimePicker and PlDateTimePicker work from the keyboard. No row could take focus, so the only way to set a time without a pointer was the Now button. Each column is now one tab stop on its chosen row, and the arrow keys, Home and End choose a row and move the focus with it, stepping over a blocked row.

  • A PlToast that is fading out stays closed. Raising another toast, or moving the pointer off the stack, during the fade gave the closing toast a new clock, so five seconds later its onClose was called a second time and its disposed fade threw. A closing toast is now off the clock and cannot be dismissed twice.

  • A sortable PlDataTable heading can be reached and pressed from the keyboard. It answered a pointer only, so Tab passed it by and there was no way to sort without a mouse or a touch screen. It is now a focus stop with a focus ring that Enter and Space press, as the React build's heading button is.

  • A PlHeatmapChart treemap names each tile after its own point. A tile's name was read from the column at its index, which on a treemap is the name of the tile in that place in the first group, so every other group's tiles, their tooltips and the screen reader summary borrowed the first group's names.

  • A series that starts hidden can be switched back on from the legend. The chart kept reading hidden as well as the legend's own switches, so pressing the entry changed nothing. hidden now only sets where a series starts, as it does in the React build.

  • A narrow cartesian chart writes its category names whole again. Every name was cut to its slot, so on a phone twelve months read J…, F… and thirty days read , and the value ticks of a PlScatterChart were cut too. As in the React build, a name is now cut only when the slot has room for about four characters, a narrower axis thins its labels out instead, and a tick is never cut.

  • An open PlModal, PlDrawer, PlOverlay or PlConfirmProvider sheet takes the page behind it off the semantics tree. The backdrop stopped the pointer and the focus scope stopped Tab, but a screen reader could still move onto the page underneath and press its buttons. The page comes back as the layer starts to close.

  • Pressable rows answer a screen reader. A PlTree row, a PlMenu row, a PlNavigationMenu word and panel link, a reachable PlStepper step, a PlAnchor row, a PlImage with preview, a PlOtpField and an interactive chart legend entry had no tap action on their semantics node, so TalkBack, Switch Access, Voice Access and a screen reader on the web could not activate them. The legend entries also take keyboard focus and a focus ring now, a navigation word says whether its panel is open, and a step ahead of the reader is no longer announced as disabled.

  • The arrow keys keep focus in PlTabs, PlRadioGroup and PlSegmentedButton. When the value followed the arrow, the item that lost the stop gave focus back to whatever was focused before the group, so a reader who had tabbed in changed the value once and was thrown out. Focus now moves to the newly chosen item.

  • A PlImage fades in again. The picture was meant to fade up once its first frame arrived, but that frame also took the placeholder away, and the fade was rebuilt under a different parent in the same build. A rebuilt fade starts at full opacity, so every picture that had to be waited for cut in. The fade now stays where it was and runs.

  • PlSpoiler no longer changes height when it is uncovered. The cover is a line of explanation and a button, so it is routinely taller than the text it covers — and it was taken out of the layout on reveal, which collapsed the sheet to its content and pushed the whole page below it up. Covering it again pushed everything back down. The cover now keeps its place in the stack and is held hidden with Visibility(maintainSize: true) and ExcludeFocus, exactly as the reversible hide row already was, so the sheet measures the same in both states and the hidden cover is off the semantics tree. A maxHeight clamp is still released on reveal, which is the one thing that may resize it.

Changed #

  • A PlSkeleton runs one animation for all of its bars. Each bar ran its own ticker and clip, so a list of twelve three-line placeholders ran 36 tickers. The bars of one placeholder now share one, and each highlight is painted inside its bar's corners without a clip.

  • PlAvatar, PlImage and the PlGallery viewer decode a picture at the size it is drawn. They decoded every file at its own size, so a 1024-pixel photograph in a 40-pixel avatar held four megabytes, and every tile of a gallery of twelve-megapixel photographs held about fifty. A plain ImageProvider is now decoded no larger than its box needs at the screen's pixel ratio: a cropped picture reaches both sides of the box and one shown whole fits inside it, rounded up to a step of 128 device pixels so a box that grows by a pixel is not decoded again. PlImage measures its box on the first frame and asks for the picture on the next, and decodes again only when the box grows. A ResizeImage you pass is used as it is, and fit: PlAspectFit.none still decodes the whole file. One consequence: precacheImage with a plain provider warms a different cache entry from the one these widgets draw, so precache a ResizeImage and pass the same one.

  • PlCombobox and PlCommandPalette say the label pack's empty when nothing matched. Their emptyMessage defaults, "No matches" and "No commands found", were written in English, so a translated application still said them in English. Both parameters are now nullable and fall back to empty, as PlTreeSelect and PlTransfer do, which makes the English default "Nothing here" for both. Pass emptyMessage to keep the old words.

  • A chart series with no name is called by its number. The legend, the tooltip and the summary of PlLineChart, PlBarChart, PlAreaChart and PlScatterChart called it "Series 2", a word no pack translates, while PlHeatmapChart, PlTimelineChart and the React build say "2". They now say the number as well.

  • A long PlCombobox or PlCommandPalette list builds only the rows near its view. Both built every row on opening and on every key, and the palette searched every command's text again on each rebuild, including the one a pointer moving over the rows causes. A list taller than its popup is now built as it scrolls, and the palette keeps its search until the query changes or its parent rebuilds. A shorter list is laid out as before.

  • Focus rings are drawn in the family's accent, opaque. PlassColorFamily.ring was solid at 55% opacity, which came to about 2.2:1 against a white surface, and 1.4:1 for warning, under the 3:1 a focus indicator needs. It now returns accent, which clears 3:1 against the surface and the page in both themes. Every focused control looks different: darker in the light theme and lighter in the dark one.

  • A PlScrollZone no longer hands the wheel back at its ends. The pointer being on the shelf is the reader saying which of the two things under it they meant to move, and reaching the last card is not them saying something else — so whatever was behind the strip used to start moving at a pixel nobody chose, in the middle of a flick. The new overscroll default is PlassOverscroll.contain, and PlassOverscroll.auto is the old behaviour. Even auto now keeps a gesture that was scrolling the strip a moment ago, and gives the signal up only once the reader has paused. A strip everything fits in is not a scroller and holds nothing back either way.

Documentation #

  • The floating action button page lists what the Flutter widget takes. It said the widget takes everything a PlButton does, including readOnly, but PlFloatingActionButton has no readOnly, density, onLongPress, focusNode or autofocus, and the Flutter props table left out the color, loading and disabled it does have. The page, the table and the dartdoc now match the widget, and the corner and floating examples have Dart.

  • The calendar page no longer promises Flutter a grid role or a year step on Shift. Its Accessibility section said, for both packages, that the calendar is a role="grid" and that Shift with PageUp or PageDown moves by a year. Both are React behaviour. The Flutter half now says what the widget does: each cell is a button, and a blocked day keeps its focus node and is announced as unavailable.

  • The card page no longer says Flutter has no heading depth. Its differences table said Flutter's semantics tree has a heading flag and no depth, but Semantics.headingLevel exists, and PlTypography already uses it. The table and the Accessibility section now point to PlCard.headingLevel.

  • The code block page no longer promises Flutter a focusable code region. Its Accessibility section said, for both packages, that the code is a focus stop named after title, that Mod+A selects only the block, and that the line numbers stay out of the selection. All three are React behaviour. The Flutter half now says what the widget does: the code is a semantics node named after codeLabel, the language or the word for code, with no focus stop and no select-all of its own.

  • The line chart page lists the fields of PlChartAxis, PlChartLegend and PlChartTooltip, which the props tables named without describing. The bar, area and scatter chart pages link to them.

  • The documentation was audited and rewritten for accuracy and plainness, in both locales and for both packages: three wrong claims corrected, 135 essay-style headings renamed after what the section covers, the em dashes taken out of the prose, and the Korean pages rewritten so they read as Korean rather than as a translation. This package's README.md carries the shortened introduction with them.

1.3.0 - 2026-09-05 #

Added #

  • PlWindowPane. A window, drawn the way one of eight systems draws it.

    A frame that behaves rather than a picture of one: the bar drags, the three buttons are real buttons with real names, and minimized rolls the window up to its bar because a page has nowhere to send it. internal/window.dart holds the reference data — eight systems' worth of title bars, three buttons each, drawn five different ways — and every glyph is a CustomPainter.

    Three differences from the React build. controls is a Set where React takes boolean | array, since the order is the system's anyway. active is a plain value rather than something the widget works out: there is no document to listen to, and a widget that reached across the tree to find the other windows would be inventing a desktop. And there is no resizable — the eight handles are a pointer affordance that a Flutter caller would more naturally build with their own gesture detector around the window.

    explicitChildNodes is what makes the window a named container rather than one long label: without it the title, the buttons and every word of the content merge into the node's own name. It is the Flutter half of what aria-labelledby does in the React build.

  • Four words in PlassLabelsminimize, maximize, restore and resizeWindow — translated in all six locale bundles.

  • PlMockup. A device with a screen you can put anything on.

    The screen is a real viewport at the device's own resolution and the whole device is scaled once into the room it has, so the content is laid out against a screen rather than against the page. internal/mockup.dart holds the reference data — five resolutions per device, three shells, six systems' worth of chrome — and every glyph in that chrome is a CustomPainter rather than an asset.

    Two differences from the React build, both because there is no CSS to lean on. wallpaper takes a Decoration where React takes a background string, and there is no scroll: a Flutter caller puts a scrollable in the child, which is the framework's own answer and a better one than a flag.

  • PlTimelineChart. Work against time — a row per thing, a bar per stretch of it.

    A bar chart on its side with the baseline taken away, and not PlTimeline, which is a list of steps and draws no axis. Overlapping spans on one row move onto lanes of their own by the greedy interval packing; a row with no overlaps is exactly as thick as it was.

    The time axis came with it. internal/chart.dart gains PlChartTimeUnit, timeScale, TimeScale, formatTimeValue and formatTimeTicks, matching the React half function for function: the steps a clock and a calendar actually have, walked with DateTime so a month is a month and a daily axis stays on midnight across a daylight-saving change. The month names come off PlDateNames rather than a platform formatter, which is the same trade the date pickers make — this package takes no dependency on package:intl.

    The frame grew three things for it: a scale a chart can hand over already worked out, a markHeading for marks that name themselves rather than taking their series' name, and rx/ry on PlassChartMark so a two-hundred-pixel bar is hit-tested against its body rather than against its centre — measuring to the centre would hand a row's short bar a press the reader is plainly not making.

  • PlassTimelinePoint and PlassTimelineSeries in types.dart.

  • PlHeatmapChart. A magnitude per cell, coloured rather than measured.

    A grid for two categorical axes, or a squarified treemap for parts of a whole. Colour encodes size and not identity, so it comes off a one-hue ramp rather than the categorical palette, and one ladder covers the whole chart.

    internal/chart.dart gains PlChartScaleKind, rampStep, squarify and TreemapTile, matching the React half function for function. PlassTokens gains chartSequentialOn and chartDivergingOn: the ink a label wears on each step of a ramp, per step rather than one answer, because the ramp crosses the point where ink stops reading at a different rung in each theme. PlassChartScaleLegend joins internal/chart_frame.dart — its own widget rather than the swatch legend with different content, since what it names is a scale and there is nothing on one to switch off.

    Two things the Dart side does differently, both forced by the absence of unions: valueLabels is PlHeatmapLabels and scale is the re-exported PlChartScaleKind.

  • PlGaugeChart. One number on a scale that is known in advance, drawn as a dial.

    A PlMeter bent into an arc, sharing its vocabulary exactly, and not a PlPieChart with PlPieShape.semi: the unfilled part of the arc is the rest of the dial rather than a second category.

    The reading is a real widget in a Stack over the painting rather than something painted, so it takes the caller's own center and caption and stays in the semantics tree. Its size is solved against the chord the hole leaves rather than fixed. The arc animates with a TweenAnimationBuilder on the swept fraction, at the same fillDuration a meter's bar uses, and honours MediaQuery.disableAnimations.

    ticks is int? where React takes number | false — Dart has no union to put a count and a switch into one prop, so null is off.

    ringPath joins internal/chart.dart: the centre line of a band as an open arc, which is what a stroke can travel along and a filled wedge cannot.

  • PlassThreshold, in types.dart, with the band rule in internal/threshold.dart. PlMeter and PlGaugeChart read the same list the same way. PlMeterThreshold stays as a typedef of it, so nothing a caller writes has moved.

  • PlSparkline. A chart with everything taken away except the shape.

    A word-sized strip that goes in a line of text and says which way something has been going, sized against that line rather than against the page. It scales itself to its own range, which is what makes it legible this small and what makes two of them incomparable until they are given the same min and max.

    The one thing it does not share with the React build is how the colour is named: color takes a PlassColor family and tint takes an exact Color, where React has a single prop typed PlassColor | (string & {}). Dart has no union to flatten, and one prop that accepted both would have to guess which was meant.

    Without a semanticLabel the whole strip is wrapped in ExcludeSemantics, which is the Flutter half of what React does by putting aria-hidden on it.

  • PlScatterChart. Two numbers per point, and whether they move together.

    Both axes measure, so this is the only chart with no categories, and a point with a z is a bubble rather than a dot. A z is an area and not a radius: encoded as a radius, twice the value draws four times the mark.

    The cartesian frame grew what a non-columnar chart needs, which is most of this change. PlassChartAxisScale.value turns the category axis into a second number line — its ticks become the axis' categories, so every measurement downstream stays one code path. PlassChartMark and a builder replace the column hit test with a nearest-mark search, markInset reserves room on all four sides for marks drawn from their centre, swatch lets a chart put its own shape in the legend, and markReadout and semanticValue let it say what a mark is worth. chart.dart gains categoryNumber, pointX, categoryExtent and bubbleRadius, matching the React half function for function.

    shape is PlScatterShape, a seven-member enum, where React takes MarkShape | 'varied' | 'auto': Dart has no union to flatten, and the two policies and the five shapes are the same choice made once.

  • PlPieChart. Parts of a whole, at a glance.

    An angle is a poor thing to compare, so a pie answers one question — is one of these most of it? — and anything finer is a PlBarChart. It takes one list of slices rather than a list of series, because the slices are the entities here: each takes a palette slot of its own, and its colour follows the slice rather than its size.

    The first chart that is not drawn in the cartesian frame, so two pieces of that frame became shared rather than private: PlassChartLegendBar, because a slice plays exactly the part a series does in a legend, and PlassChartTooltipCard, so a readout is the same glass wherever it appears. arcPath joins them in internal/chart.dart, taking its angles in degrees clockwise from twelve — the frame a caller writes a startAngle in.

    valueLabels is PlPieLabels, a two-member enum, rather than the four-member PlassChartValueLabels the cartesian charts take: last and extremes have nothing to mean on a chart whose slices are one number each.

    A press leaves the readout up and a second press on the same slice takes it down, which is what the cartesian frame does with a column and for the same reason. There is no keyboard walk here, matching the rest of the Flutter build.

  • PlBarChart. Lengths, compared.

    A bar says how much by being longer, which is why its axis starts at zero and cannot be talked out of it. A gap is a bar that is not drawn: a zero-length bar and a missing bar are the same picture and only one of them is honest.

    The frame learned to run either way round with it. horizontal swaps which band each axis reserves, which way the grid and the crosshair go, which axis the pointer runs along, and which edge the category labels sit on — and a horizontal chart's labels are stacked, so what the stride has to clear is a line of text rather than the width of a word.

    PlassTokens gains chartGrid, chartAxis and chartBaseline, so the two builds draw a chart's furniture from the same three greys.

    stacking is three named states rather than React's boolean | 'full', as on the area chart.

  • PlAreaChart. A line with the space under it filled — which changes what the chart is about.

    A line says where a value went; an area says how much of something there was, and stacked it says how that amount was made up. If the quantity does not add up to anything, the fill under it is decoration.

    The line and the band come out of one painter, internal/chart_line.dart, which is the Dart half of the split the React build makes for the same reason: an area is a line with the space under it filled, and writing the path arithmetic twice would mean a smooth that curves differently depending on which widget a caller reached for. areaPath grew a second edge with it, because a stacked band's floor is the band below it and moves with every category.

    stacking is one enum with three states rather than React's boolean | 'full'. Dart has no union type, and three named states read better than a boolean with an exception bolted onto it.

  • PlLineChart, and the foundation every chart after it stands on. A value against time, or against anything else with an order to it.

    A gap is never a zero. PlassChartDatum.gap() breaks the line, and a point with a gap either side is drawn as a dot rather than dropped. A chart that renders missing data as zero reports an outage as a collapse.

    PlChartCurve.smooth is a monotone cubic and not a spline: a plain spline overshoots between two close points, so a series that never goes below zero would draw a curve that does.

    The palette is eight hues in a fixed order, on PlassTokens.chart, and it is the one place in the library where a colour is not a semantic role. Slots follow a series' index in the list it was passed, never its position among the visible ones. chartSequential and chartDiverging are the two five-step ramps, for a mark whose colour is a magnitude rather than an identity.

    internal/chart.dart is the same arithmetic the React build runs, function for function — the two must not disagree about where a tick goes, and the only way to be sure of that is for it to be the same arithmetic. internal/chart_frame.dart is the axes, the grid, the crosshair, the legend and the tooltip, painted rather than laid out as widgets.

    A tap leaves the tooltip up and a second tap on the same column takes it down, which the React build has no need of: clearing it on the release would be a tooltip a reader with no pointer never gets to read, since on a touch screen the press and the release are a tenth of a second apart. A drag scrubs along the axis.

    PlChartLegend(hidden: true) is how this side spells React's legend={false} — Dart has no union type — and its side and align are the library's own PlassSide and PlassAlign.

    One new label, chart, in all seven packs.

  • PlGallery. A set of pictures, arranged — four layouts, captions, a pointer treatment and an optional viewer, with everything but the arrangement identical across all four.

    grid is a contact sheet, masonry keeps each picture's proportion and stacks the columns, justified keeps the proportions and fills every row to the edge, and quilted is a grid whose tiles span more than one cell, packed densely so a tile too wide for the space left drops to the next row that fits it and a later, narrower one fills the hole.

    A masonry deals across before it deals down, and the dealing is internal/gallery.dart's — the same arithmetic the React build runs, because a masonry with two orders is a masonry whose order means nothing.

    Two of the layouts measure and the React build's do not. CSS does a justified row with flex-grow and a quilt with grid-auto-flow: dense; there is no such thing here, so both pack themselves inside a LayoutBuilderjustifyRows and quiltCells, which have no React counterpart and are not an oversight. The arrangement is the same; what differs is who computed it. A justified layout's last row is left at its natural height rather than stretched, because one landscape photograph scaled to fill a row is one enormous picture under a wall of small ones.

    preview opens a viewer with the rest of the set an arrow key away — bound on the sheet rather than on its buttons, since the focus is wherever the reader last put it. It is deliberately not a PlCarousel: no autoplay, no wrap, and the arrows stop at the ends.

    A tile is a Semantics button only when pressing it does something, and its name is the picture's own words plus where it sits. One new label, gallery, in all seven packs.

  • PlCodeBlock. A viewer for one line of code or a thousand — a bar over it, numbers down the side, a prompt in front of every line, and twelve palettes to read it in.

    Everything it draws above the code is optional and off one parameter each, because the same widget has to be a snippet inside a sentence — no bar, no numbers, no chrome — and the full transcript at the top of a README.

    It is the one surface in the library that is not made of glass. Every other sheet here is translucent and takes the screen's colour family; this one paints its own opaque ground and refuses the family entirely, because the palette is the subject rather than the setting.

    Twelve palettes: four the house's — dark, light, auto and mono, which has no hue in it at all — and eight ports kept at the hex they were published in. A PlCodeTheme is fourteen colours and not nineteen: the dim ink, the rule, the hover wash and the two a marked line uses are all derived from the ground and the ink, so a palette of your own is customTheme and fourteen values.

    It does not colour the code and the React build does. That side reaches highlight.js through a dynamic import; this package has no dependencies, and a hand-written grammar for thirty-five languages is a promise it could not keep. So a caller who has a highlighter hands the result in as lines — runs of text with a PlCodeTokenKind on them, the same twelve slots a theme declares — and a caller who does not gets the frame, the twelve palettes and the code drawn in one ink.

    highlightLines is a String here and a number, string or list on React: '4', '4-9' and '1,4-9,12' cover every case, and a Dart Object? that had to be type-tested at runtime is not an API.

    Five new labels — copy, copied, copyFailed, raw and code — in all seven packs.

  • PlTreeSelect. A value chosen out of a hierarchy rather than out of a list — a PlTree behind the same field every other picker wears.

    It is the two of them composed and almost nothing else. What the widget adds is the arithmetic between them: which nodes a query keeps, which branches that opens, and which of the ids coming back out of the tree are answers rather than roads.

    selectableBranches is off by default, which is the shape most of these trees actually have: the branches are the taxonomy and the leaves are the answers. A node's own selectable overrides it either way, and a branch that cannot be chosen still opens and closes — pressing it is how you get at what is under it. Turning that press down is deliberately not the same as clearing, or a single-value picker would empty itself every time somebody opened a folder.

    searchable keeps every match and every ancestor of one, opens every branch it kept, and hands the folds back untouched when the field is emptied. The fold is case-only, as everywhere else in this package: Dart's core has no String.normalize, and the React build's accent folding is the one thing the two searches do differently.

    A node's label is a String and not a widget, which is the divergence PlTransferItem already carries and it is here for the same reason: the filter reads it, the trigger writes it and a screen reader is handed it. Text is what keeps every node searchable by construction, and it is why there is no searchLabel on this side — the label is already the words.

    value is a controlled Set<String>, as every input in this package is. expanded and open are the two that may be left out, and then the picker keeps them itself.

    No new label: search, empty and clear were already words this package says.

  • PlTour. A guided walk over a screen that already exists — the three things a new reader has to be shown once, pointed at where they actually are.

    It is PlHowToSteps turned inside out: that widget puts the instructions in the screen and the reader follows them, and this one leaves the screen as it is and stands over it. So a step says what it is about rather than describing it.

    The dimming takes the pointer and the light does not. The scrim is one layer over the screen with the target cut out of it by a ClipPath, and a clip takes hit testing with the painting — so the control being pointed at goes on working and nothing else does. It is also what lets the dimming carry a BackdropFilter: four rectangles around a target can only paint a colour, and the corners of a four-piece scrim never quite meet.

    It is deliberately not built on the internal portal every other layer in this package uses. That helper holds focus inside itself, which is right for a modal and wrong here: a tour whose reader cannot reach the control it is pointing at has pointed at a picture. So this one lifts itself into the Overlay, keeps Escape, and takes neither the route nor the focus.

    target is a GlobalKey and nothing else — every widget on the screen was written by somebody who can put a key on it, and a key is checked by the compiler where the React build's selector is a string. For a screen that scrolls, controller is the same parameter PlAnchor takes and for the same reason: the tour is up in the overlay and cannot see a scroll notification from down there.

    The card's buttons wrap to a second line rather than running off the edge, because a translation whose words are longer than English's is three buttons wider than the card. One new label, skip, in all seven packs.

  • PlDataTable. A table that owns its rows: it sorts them, narrows them to what was typed, hands them out a page at a time and remembers which of them are ticked.

    PlTable is now a thin wrapper on the same grid. Everything below the columns — the measured column widths, the hover band, the one focus stop per row, the rule between rows and the header pinned over the scroll — moved into internal/table.dart as PlassGrid, and both widgets build one. PlTable's own behaviour is unchanged and its suite passes untouched; what changed is that a second table cannot drift away from the first.

    Sort, search, selection and page are each uncontrolled by default and controllable one at a time, so the ordinary table is columns and rows and a server-backed one is the same code with manual and four callbacks. The starting values are initialSort, initialSearch, initialSelected and initialPage, which is this framework's own convention rather than React's default….

    Sorting rotates ascending, descending, then back to the order the rows arrived in — that order is usually the server's, and a table that can never be put back has thrown it away. Nothing sorts last in both directions. Text compares case-insensitively but does not fold accents, for the reason internal/search.dart already gives: Dart's core has no String.normalize and this package has no dependencies. The React build uses localeCompare and the page says which is which.

    value is required on any column that sorts or searches, not only on a drawn one: cell returns a widget in every case, and a widget has no order and no text to look inside.

    A sorted heading says its direction out loud. That is the one place the two builds differ in kind rather than in spelling: aria-sort is a platform affordance every screen reader speaks in the reader's own language, and Flutter's semantics have no equivalent — so the word has to be said, and a word that is said has to be translated. sortedAscending and sortedDescending are new in PlassLabels and have no React counterpart. selectRow is new in both.

    The tick column is measured rather than given a width, because the box is a different size on every rung of the ladder; PlassGridColumn.flex gained a null for it, which keeps a column out of the share-out of whatever the sheet has left over.

    Four things it deliberately does not do — virtualize, drag-resize or reorder columns, export, or sort on more than one column — and the page says why for each.

  • Locale bundles: the package's own words, translated. Every widget that says something of its own — a close button's name, a pager's landmark, the line an empty list shows — now reads from one set of sixty-six strings, and seven translations of that set ship with the package.

    import 'package:plass_ui/locales.dart';
    import 'package:plass_ui/plass_ui.dart';
    
    PlassTheme.merge(
      defaults: const PlassDefaults(labels: ko),
      child: const MyApp(),
    );
    

    de, en, es, fr, ja, ko and zhHans, in a library of their own rather than behind plass_ui.dart. That import is in every file of an application, and putting seven translations behind it would make every one of those files carry them.

    A key is a meaning, not a widget. close is the × on a modal, a drawer, a popover and a toast, and it is translated once. A key exists per widget only where the word genuinely differs: paginationNext moves by a page and carouselNext moves by a slide.

    PlassLabels is what PlPickerLabels grew into, and PlPickerLabels is now another name for it, so nothing that took the old one has to change. Each of its sixty-six fields still defaults to English in the constructor, which is what makes a partial set legal; copyWith is new, and it is how a screen keeps a pack and changes one line of it — ko.copyWith(start: '체크인'). PlassTheme.labelsOf(context) reads what is in scope, answering PlassLabels.english when nothing has decided.

    The twenty-four widgets that were carrying their own English default now take it from the set, and every one of them still takes the word as a parameter that wins.

  • PlAnimateSplit. A line of text arriving one part at a time — the Dart half of the React component. A PlAnimateAppear tells one entrance off across a set of children, which a line of text does not have; this makes them.

    The entrance is spelled as a side, a distance and a fade, exactly as PlAnimateAppear spells it, where the React build names a CSS keyframe. That is not an inconsistency and the page says why: over there an effect is a named thing the stylesheet knows about, and here every effect is built out of widgets, so a split takes the parameters the widget beside it already takes.

    PlAnimateSplitBy.character breaks the shaping between letters — Arabic stops joining, an emoji is cut into its pieces — and the page says so. Whitespace is glued onto the part before it rather than becoming a part of its own, which keeps a Wrap breaking lines between words and stops a gap taking a step of the stagger. The parts are excluded from the semantics and the whole line is put on the node instead.

  • PlAnimateScramble. A line of text resolving out of noise — the Dart half of the React component, and with the same decision at the centre of it: the noise is made of the line's own characters, because every scrambler that ships a default alphabet ships an English one and English noise over a Korean headline is a different script flickering rather than a word arriving.

    It settles left to right, never scrambles whitespace, and steps the redraw at tick rather than at the refresh rate. The settling line is wrapped in ExcludeSemantics and the real line is put on the node instead.

  • PlAnimateCounter. A number counting up to what it is — the Dart half of the React component, and the one widget in the package whose trigger defaults to PlassAnimateTrigger.visible rather than mount: a count that ran off screen delivered a number that was already sitting there.

    formatValue is a callback rather than an options object, for PlProgressLinear's reason. The ticking figure is wrapped in ExcludeSemantics and the answer is put on the node instead, because a number changing sixty times a second in the semantics tree is either silence or sixty announcements.

  • PlAnimateShake. A refusal — the one effect in the set that answers something the reader did rather than announcing that content has arrived, so trigger defaults to PlassAnimateTrigger.manual and it starts held still.

    replay is what it exists around: a refusal can happen twice and a bool cannot say "again". A value that has changed is the closest a widget tree has to an event, and it never plays on the first build. PlassAnimateSettings takes a nonce for it, and the gate calls restart rather than _set(true) — the second refusal has to play even though the first one already started the run.

    A damped sine that completes a whole number of cycles, so the widget lands exactly where it started rather than wherever the curve happened to be when the run stopped.

  • PlAnimateFloat. Content drifting gently and not going anywhere — the odd one out among the PlAnimate* widgets, because every other one is an entrance played once when content arrives and this one never finishes.

    The cycle is symmetric, home to out to home, so a run that ends leaves the widget exactly where it found it. curve defaults to Curves.easeInOut rather than to the house curve: the house curve is an entrance's, and a drift with it would lurch at each end of the cycle instead of turning around.

  • PlHowToSteps and PlHowToStep. Instructions, numbered, with what to do under each one — the Dart half of the React component. A PlStepper and a PlTimeline say where you are; this says what to do, so every step's body is open at once.

    The position is written into each step's semantics, which is the one place this parts company with the React build: there a real <ol> makes a screen reader say "list, five items, item two" for nothing, and Flutter has no ordered list to inherit that from. semanticStepLabel says the words, and it is a callback rather than a pair of strings for PlProgressLinear.formatValue's reason — there is no Intl in the framework, and pulling package:intl in would be a dependency decision made on a consumer's behalf.

    The bullet, the halo and the connector come from internal/steps.dart, the same place the stepper and the timeline take theirs.

  • PlFloatingActionButton. The one action a screen is about, floating over it — the Dart half of the React component, and a PlButton or a PlIconButton underneath depending on whether the words are drawn.

    label is required and is always what a screen reader hears, whether or not extended draws it: an icon-only button with no name is the defect this pattern ships with everywhere else.

    While floating it is a PositionedDirectional, so it belongs in a Stack — which is what a screen's body usually already is once anything floats over it — and the corner is start/end rather than left/right, so it crosses the screen under RTL with everything else.

  • PlAnchor. A table of contents that follows the reader down the screen — the Dart half of the React component, and the same rule: the lit row is the last heading whose top has passed the reading line, nothing is lit above the first, and the last row is lit at the bottom whatever the measurement says.

    An item points at a GlobalKey rather than at a fragment, because a Flutter screen has no URL to point into: what is tracked is a render object's position, and a key is the only handle on one. A press calls Scrollable.ensureVisible.

    The bottom rule is guarded the same way the React one is: only where there is something to scroll, because a screen that fits is always at its own bottom.

    It measures against the scroll view rather than against the list, which matters for the ordinary arrangement: a list pinned beside the page moves with it, and measuring from the list itself would make every answer depend on where the list happens to be.

  • PlDataList and PlDataListItem. A list of labels and the values that go with them — the Dart half of the React component, and the panel every detail screen ends with.

    What a <dl> does over there, MergeSemantics does here: each label and its value are announced as one pair. A label read on its own is a word, and a value read on its own is a fact nobody can place. That is the whole reason it is a widget rather than a column of rows.

    The label column is a fixed width rather than the longest label, so two panels on one screen line up and a value does not move when a field is renamed. divider rules between the rows and only between them, and the list draws those lines rather than the rows, because a row does not know whether it is the first one.

  • PlAppLogo. A product's mark and its name beside it — the Dart half of the React component. PlAppLogoShape.bare is the default for the same reason: a mark drawn with its own background or its own margin is finished artwork, and a plate would give it two edges.

    The mark is a widget rather than a URL, so it is whatever draws the artwork — a PlImage, an Image.asset, a CustomPaint — and semanticLabel is what a screen reader hears when there is no name. With a name the mark is wrapped in ExcludeSemantics, because a wordmark beside a picture of the wordmark is the product's name read twice.

    onPressed is the Dart shape of React's render={<a href="/" />}: a logo is nearly always the way back to the front screen, and leaving it null draws the same logo and presses nothing.

  • PlHoverCard. A preview of what is behind a link, shown when the pointer rests on it — the Dart half of the React component, and the third of the floating surfaces. What separates it from a PlTooltip is that the pointer can move onto it and what is inside can be pressed; what separates it from a PlPopover is that nobody asked for it.

    The pointer is tracked on the trigger and on the card, as two flags rather than one. That is not tidiness: the gap between them has no pointer in it, so a single flag would close the card on the way to it and it could never be reached.

    delay is 600ms so the card does not fire at every link a pointer crosses, and focus opens it too — a reader who tabs onto the link gets the same preview, which is the half a hover-only card loses.

    The wedge moved out of PlPopover into internal/wedge.dart on the way, because two surfaces now draw it and a second copy of a triangle painter is a second place for the hairline to stop lining up with the sheet's.

  • PlScrollArea. A bounded box that scrolls, with the library's own scrollbar in it — the Dart half of the React component. Built on RawScrollbar from package:flutter/widgets.dart, because the framework's own Scrollbar lives in material.dart and this package imports neither that nor cupertino.dart. The thumb is PlassTokens.track, the same neutral ink a PlSlider's rail and a progress groove are cut in.

    height is a parameter rather than an enclosing SizedBox for the reason the React prop is one: a vertical scroll area has to be bounded by something or its content has nothing to overflow, so it is the measurement without which the widget does nothing. maxHeight is the ceiling form.

    orientation: PlScrollAreaAxis.both is two scrollables, one nested inside the other, and each bar answers only its own through a notificationPredicate. A horizontal bar that moved when the page scrolled down would be reporting the wrong axis.

    scrollbars: PlScrollbars.auto shows the bars while the pointer is over the box or the content is moving, which is what the React build's hover state does — a MouseRegion rather than a scroll-only fade, because a reader deciding whether there is more below has not scrolled yet.

  • PlMeter. A quantity inside a range, drawn as a bar — the Dart half of the React component, out of the same groove a PlProgressLinear is cut into and deliberately not one of them. Progress is something advancing; a meter is something already known, so value is required, there is no sweep, and there is no indeterminate case to have a default for.

    thresholds is what it exists for: PlMeterThreshold(from:, color:) bands where the highest from at or below the value wins, read rather than walked so their order does not matter, and from is in the meter's own units rather than a percentage.

    The semantics are where the two builds genuinely differ, and it is written down rather than left to be found. SemanticsRole has no meter, and claiming progressBar would announce the one thing this widget exists to deny — so it reports a named node carrying a value and no role at all. That is what the platforms read out for either one in practice; what is given up is the role name.

    formatValue is a callback rather than an options object, for the reason PlProgressLinear's is: there is no Intl.NumberFormat in the framework, and pulling package:intl in to provide one would be a dependency decision made on a consumer's behalf.

  • Six widgets turn with the window. orientation on PlPanes, PlTabs, PlScrollZone, PlTimeline and PlStepper, and direction on PlStack, all take a PlassResponsive — so a tab bar is a column on a phone and a row on a laptop from one argument.

    Resolved against MediaQuery.sizeOf(context).width in build, so it is right on the first frame; there is no server render here to disagree with, which is the one place this is simpler than the React build. It is the window's width rather than the widget's own box, as every breakpoint in the package is: two of these side by side agree about which rung they are on however wide each of them ended up.

  • maxWidth is responsive and takes an exact width, on PlContainer, PlHeader and PlFooter alike. PlassResponsive<PlContainerWidth?>PlContainerWidth.rung(PlassSize.lg) is a rung of the measure ladder and PlContainerWidth.pixels(720) is a width, two constructors rather than one nullable pair because only one of them can be true at a time and Dart has no untagged union. null still means no limit, including as an entry, so a screen can be uncapped on a phone and capped from md up.

    The exact width is the one worth having: the ladder is five numbers chosen against the breakpoints, and the measure a paragraph actually wants is a count of characters at whatever size it is set in. No ladder can spell that.

    The three widgets each carried their own copy of the ladder and now share one — a bar whose measure did not line up with the container under it is a screen with a visible seam down one edge, and a test asserts the two agree rather than asserting a number twice.

  • PlShow. Content at some widths and not others — from, until, or both as a band, on the same ladder everything else uses. until is exclusive, which is what lets it and from be the two halves of one decision: no width draws both and none draws neither.

    It builds nothing at all at a width it is closed at — not an empty box with a size, and not an Offstage subtree. That differs from the React build, which sends both halves and hides one with display: none, and it cuts both ways: an expensive subtree is free here while it is closed, and any state inside one is lost when the window crosses the boundary. Lift that state above the gate.

    PlassBreakpointFloor comes with it — the four rungs that have a floor to ask about. xs has nothing below it, so from: xs would mean "always" and until: xs would mean "never".

  • PlAnimateReveal. Content uncovered behind a moving edge, and the only entrance in the set where nothing moves and no colour changes. A fade changes the ink, a slide changes the position, a grow changes the size; this changes how much of the widget is painted and leaves every pixel it has painted exactly where it will finally be — which is what makes it the effect for anything whose position is the information: a heading over its own paragraph, a divider between two sections, a chart's plot area, a column of figures.

    A ClipRect with a clipper rather than an Align with a widthFactor, and the difference is the one this effect exists for: an Align resizes the box and everything beside it moves, while a clipper is asked for a rectangle at paint time and the layout never hears about it. from is physical — PlassSide is everywhere — and PlassAnimateMode.exit closes from whichever edge it opened towards. fade is off by default, alone among the effects that offer it: fading a reveal asks for two entrances at once.

Fixed #

  • A PlMockup drew its wallpaper over the screen instead of behind it. The decoration was handed to the screen container's foregroundDecoration, which paints after its child, so anything opaque — a DecorationImage, the usual case — covered the content, the status bar, the dock and the taskbar. The field's own documentation said "what is behind the content" and the React build has always painted it there, so the two halves of the same picture disagreed. It is the bottom layer of the screen's stack now rather than the container's decoration, because that slot is already holding the screen's radius and the ring the hardware cuts around it. A test asserts the wallpaper is never drawn at DecorationPosition.foreground.

  • A PlImage replaced its placeholder between two frames. The picture fades up over it now, on the house duration, which is what the React build does and now says for both. A photograph that cuts in reads as the layout changing its mind, and it reads that way hardest on the slow connection the placeholder exists for. The AnimatedOpacity is built while the picture is still loading rather than once it has arrived, because a widget created at 1 has nothing to travel from. A picture the provider had already decoded — wasSynchronouslyLoaded — is handed back whole and unwrapped: an entrance for something that never had to be waited for is an entrance for nothing.

  • A PlTree's branch dropped in and vanished rather than opening. It was the last fold in the package that did not travel: a PlAccordion, a PlCollapsible and a PlPill all move a height over 260ms, and this one swapped its rows in and out between two frames — under the twisty that had just been pressed, which is exactly where a reader is looking. It opens on the same PlassFold they do.

    The rows are nested rather than flat to make that possible: a branch that travels is a branch whose rows are laid out inside one box, and a flat column has no box to give it. What the arrow keys walk is still the flat list, because where a row is and how it is reached are different questions. Folds nest exactly — a height factor asks its child for whatever size it currently is, so an outer branch contains an inner one opening inside it frame for frame.

    A shut branch is not built at all: the fold takes a callback rather than a widget and only calls it when there is something to show. That is one place this build costs less than the React one, which builds the elements and throws them away, and the tree page says so.

  • A PlSlider's thumb teleported to any value nobody dragged it to. An arrow key, a press on the rail, a value set from outside — the thumb was in one place on one frame and somewhere else on the next, on the one widget whose whole subject is where along here. The thumb and the run are AnimatedPositionedDirectional now and travel over the house duration.

    Duration.zero while a finger is on it. A thumb that eased towards the pointer would be a thumb lagging behind it, which reads as the widget being slow rather than as the motion being smooth. What is animated is the position the value is written into rather than a Transform, which is why this does not bend the no-transform rule: nothing is shifted off its own place, because the thing being moved is the value.

  • A PlTree's closed twisty pointed at the screen's right under RTL. It named a quarter turn and never asked which way the rows ran, so in a right-to-left tree it pointed back up the hierarchy rather than along it. The React build has always turned it the other way; this one does now. AnimatedRotation is a paint-time turn and knows nothing about the direction it paints in, so the direction is read by hand — which is safe here for the reason the RTL page gives for a chevron: the glyph is square, so no part of the row's layout depends on which quarter it lands in.

  • A PlTree's twisty jumped between its two angles. It was the one chevron in the package still drawn with RotatedBox, which turns the layout and cannot be animated — every other one is an AnimatedRotation, and this one is now too. It matters more than the size of it suggests: the twisty is the only thing on a row that says whether the branch is open.

  • A PlCheckbox and a PlRadioGroup cut every animation they had, and the tick and the dot had none to begin with. Both widgets passed lit: marked to plassStateFilter, and lit is what decides whether the brightness filter is in the tree at all — so flipping it with the value swapped the widget at that slot and threw away the whole subtree under it. The tick's own AnimatedContainer was starting over from nothing on every tap, which is why the glass-to-gradient swap was a hard cut rather than the 150ms it was written as. The rule the two of them broke is worth stating: a wrapper that comes and goes with a value destroys the state of everything under it. The same thing is said by hovered, which was already saying it, and saying it once is enough.

    With the tree steady, the marks animate. The tick draws itself along its own path — measured with PathMetric and cut at the fraction laid down, which is the same statement the React build makes with a stroke-dashoffset. The dot grows out of the middle of its ring, as a box and not a Transform: the ring centres a fixed-size child, so both ends of the change are laid out about the same point. Both are built in either state, because a mark thrown away the moment the value is cleared cannot travel back out.

  • A PlTabs bar with more tabs than room scrolls, rather than overflowing its box. Eight tabs in a 240px box was a RenderFlex overflowed by 474 pixels, and the documentation's answer — wrap it in a SingleChildScrollView yourself — does not work: a scroll view around a PlTabs scrolls the panel along with the bar. The strip scrolls on its own now, inside the trough and inside the rule, so neither of those travels with the tabs. It comes with the signal the React build gained in the same cycle: a ShaderMask fades the end that still has tabs behind it, and only that end. A bar whose tabs all fit still shrink-wraps to them and pays for no compositing layer at all. The gradient is laid out with AlignmentDirectional, so the two ends stay the reader's two ends under RTL.

  • A PlScrollZone's inline scroll button now stands in its lane instead of hiding in it. With buttons="auto" and the default inline placement, the button with nowhere to go was drawn invisible so that its lane would not come and go and resize the strip under the pointer. The lane was right and the emptiness was not: the space is paid for either way, and a reserved empty lane beside a strip does not read as restraint — it reads as odd padding on one side of the box. It is disabled and visible now, which is what buttons="always" already drew in the same position, so the two settings no longer disagree about what "nowhere to go" looks like. What auto decides is whether the strip has scroll buttons at all. An overlay button is unchanged and still goes, because there the space costs nothing to give up — remove a control when its absence is free, disable it when it is not.

  • A reversible PlSpoiler changed height when it was opened, and again when it was closed. The Hide row was drawn only once the spoiler was uncovered, so revealing grew the sheet by the height of a button and covering it again shrank it back — the page moving twice around the control somebody is pressing. The row is built from the start now and merely held invisible and out of reach under the cover, so its space is paid for once. The generalisation is worth keeping: a control that appears with a state should reserve its space in the other state, because the space is the part a reader notices.

  • A ghost PlToggle that was off drew itself as one that was on the moment the pointer arrived. Its hover was the family's soft, and soft is exactly what a ghost toggle on is painted with — so the two states differed by their ink alone, which is not a difference a reader is going to read. The hover climbs the neutral glass ladder in all three variants now, the same two rungs solid and glass were already using. The rule is the one a two-state control fails quietly: a control with two states cannot put the colour family on the false one, and a hover is still the false state.

Changed #

  • A surface that takes the page now opens at 260ms rather than 150ms. PlModal, PlDrawer, PlOverlay and PlCommandPalette — everything built on PlassPortal, sheet and scrim together, since the portal owns the fade for all four. PlassTokens.durationSlow already existed and was already what a height travels over; it was never applied to the thing it describes best.

    150ms is a key going down. Across the whole window it is not a fade, it is a cut with a hint of blur on it, and a page that changes this completely that fast leaves a reader looking for what moved rather than reading what arrived. The line falls exactly between the two portals: PlassAnchoredPortal — a menu, a popover, a tooltip, a select's list, a picker's sheet — stays at the control duration, because a popup that hangs off a control is the size of one.

    Nothing about the shape of the motion changed. It is still opacity and nothing else, in both directions, on the same curve.

  • PlStack replaces PlAvatarGroup, and this is a breaking change. A row of overlapping faces is one arrangement of a pile rather than a widget of its own, so PlStack takes whatever it is given and never looks inside. max, total and overlap carry over; direction, drop, front, scaleStep, opacityStep, overflow and ring are new.

    The overlap is real layout, never a Transform. A translated pile is laid out one item wide, paints outside its own box, and everything after it is placed against a size the reader never sees. Flutter has no negative margin — EdgeInsets asserts it is non-negative and so does Flex.spacing — so the pile is laid out by a render object of its own, which is the only place child sizes are known. It reports the same box the React build does: five 32px items at 10px of overlap are 120×32 horizontal, 32×120 vertical, 120×72 diagonal. It grows from the reader's start, so it mirrors under RTL, and it hit-tests front to back so the item a reader can see at a point is the one their finger lands on.

    Two things diverge from React and the page says why. ring takes a BorderRadius? rather than a bool: there a ring is a box shadow and CSS gives it the element's own border-radius for nothing, and here nothing can read a child's shape. It is painted as a spread shadow, so it costs no layout and the overlap arithmetic stays about the item rather than the item plus its ring.

    What is lost is the group scope, and it could not be kept: a pile that accepts arbitrary children has no way to know one of them is a face. PlAvatar no longer reads one — size and color come off the nearest PlassTheme, and shape, variant and elevation are the avatar's own.

1.2.0 - 2026-08-31 #

Added #

  • Five fields take a hotKeys map. PlTextField, PlNumberField, PlOtpField, PlCombobox and PlSelect now bind chords: hotKeys: {'Mod+Enter': save, 'Escape': cancel}. This is the half of the story the framework does not give away — there is no prop spreading here, so a key a widget did not declare a callback for was simply unreachable, and until now that was every key on a field but onSubmitted. The chord is spelled the way a key cap is spelled, in the vocabulary PlHotKeys draws: Mod resolves per platform, and Esc, Return, Cmd and Option fold onto the same keys their caps do. A shortcut a widget displays and a shortcut it binds are now one string rather than two that can drift — the argument PlCommandPalette made when it bound its own opener, with a private copy of the matcher to make it; that copy is now internal/keys.dart, and PlHotKeys reads its alias table and its platform from the same file. Three rules hold everywhere: the map is bound closer to the focused node than the widget's own key handling, so a caller can take a key from the control; a modifier is checked in both directions, so Enter does not fire on Shift+Enter; and a chord that matches is consumed, reaching neither the control's own keys nor the route above it. PlSelect is the one place nesting cannot settle — a FocusableActionDetector binds Enter closer than anything a field can wrap around it — so the trigger stands down from Enter whenever the map asks for it, rather than the caller losing a key they bound.

  • A PlDatePicker can stop at a month or a year. precision is PlDatePickerPrecision.day, .month or .year, and it is a floor: the calendar opens on the grid for that unit and pressing a cell in it answers, so a month picker's month grid is the last grid and there is no day grid under it at all. That last part is the point rather than a side effect — a card's expiry is a month, and a control that makes someone answer which day of December 2027 is a control that will be answered wrongly. The value stays a DateTime, normalised to the start of what was chosen: the 1st of the month, or the 1st of January, never whichever day or month the cursor happened to be resting on when the year was pressed. Two things follow it without being asked for — what the trigger writes when there is no formatValue (a new PlDateNames.monthYear, which swaps its two halves the way medium already does), and the footer's shortcut, which reads "This month" or "This year" rather than "Today" and brings two new PlPickerLabels entries with it. minDate and maxDate are then read at the same precision: a minDate of 15 July leaves July pickable on a month picker and hands back 1 July, because a bound on a control that returns a month is a bound on months. shouldDisableDate is day-granular and is not consulted at all.

  • PlCalendar. The grid a PlDatePicker opens, on the page. It is not a new calendar — it is the same internal/calendar.dart the four pickers already share, with the trigger and the popup taken away, which is why it arrives with the three views, the single tab stop and the arrow keys that step the month when they run off an edge already finished. The reason to have both is what each one is: a picker is a field that happens to open a calendar and belongs in a form beside other fields, and this is a calendar that is not standing in for a field — a booking page, an availability view, a date rail — where the grid is the interface rather than a way of filling one in. So it has no label, description or error. precision is the same floor the picker's is, and the value is normalised to the start of what was chosen. Two props are deliberately absent: there is no density, because padding on a grid of forty-two squares is what stops them being squares, and no readOnly, because a read-only field still shows a value a reader can select and copy while a calendar has nothing to copy — a grid whose every cell is inert is the disabled one. autofocus is off, the opposite of the picker's: a popup has just been opened by somebody who wants to be in it, and a calendar in a page has not. It differs from the React build in the two ways every date component here does — names and labels rather than a locale string, and no name, because a Dart form is not an HTML one.

  • PlStepper and PlStep. A process the reader is moving through, and where they are in it. It draws the same rail a PlTimeline does — and shares it: the bullet, the connector, the three states and the status arithmetic moved into a new internal/steps.dart that both widgets read, because a haloed bullet must not mean two things and two copies of a painter are two copies that drift. PlTimelineStatus and PlTimelineConnector are now typedefs onto the shared enums, so nothing a caller wrote has changed. What differs between the two widgets is what each is for: a timeline reports a sequence that already happened and nothing on it can be pressed; a stepper is the sequence, its steps are buttons and the current one owns a panel.

    The steps are a list rather than children, which is PlTimeline's arrangement and for its reason — the stepper has to reason about them, and neither "which one is complete" nor "which one can be reached" can be asked of an opaque Widget. That also settles the sharp edge the React build has to warn about, since there is no wrapper to hand it. linear is on by default while a step behind the reader is always reachable; horizontal puts the panel under the rail and vertical puts each panel inside its own step. optional takes a Widget rather than a bool, because there is no default string to fall back to: the package ships no translations. The current step is marked selected, the nearest thing the framework has to aria-current="step", and it is the only role claimed — a stepper is not a tab list.

  • PlTree. A hierarchy opened one branch at a time. It takes its nodes as data rather than as children — the opposite of most of this package, and the right way round here for one reason: a tree is recursive, and recursion written in widgets is a widget every caller has to write for themselves. PlTable takes its columns the same way and for the same reason.

    expanded and selected are Set<String>, both controlled like every other input here, and each callback hands back the whole set rather than the one id that changed. selection is single by default; none makes the tree a browser rather than a chooser — rows still expand and a press still reports through onItemPressed, but nothing stays lit. children: [] and children: null are different things, visibly: the first is a branch that opens and shows nothing, the second is a leaf with no twisty at all, which is what makes a lazily-loaded tree possible. The keyboard is the tree pattern — ↓/↑ walking the rows that are actually visible, → opening a branch and then stepping into it so a reader can open one without leaving the row that told them it was there, ← closing it or stepping out to the parent, Home and End to the ends — and one tab stop for the whole thing: every row's FocusNode but the current one carries skipTraversal, which takes it out of the Tab order while leaving it in the focus tree, so the arrows can still reach it.

  • PlImage. An Image is one widget and it works, so this says what it adds rather than assuming it. Three things: the space is reserved before the picture arrives, so what is under it does not move when it does — that is ratio, and it is the parameter worth reaching for every time; a failure is drawn rather than left as an empty box, and the default drawing is the semanticLabel, which is the one thing certainly available and certainly describing what is missing; and the two are one state machine, so a placeholder is never still behind a picture that has loaded, and a changed image starts again rather than inheriting the last one's success. image is an ImageProvider rather than a URL, because that is the shape every source has in common. preview opens the picture over the page in a PlOverlay and is off by default — a picture that grows when you press it is a promise that there is more of it to see. Without a ratio the widget is the picture's own intrinsic size, which is Image's behaviour and is deliberately not overridden.

  • PlEmpty. An empty list, a search that found nothing, a request that failed and a flow that has finished are the same arrangement — a mark, a line, a sentence, a way out — which is why they are one widget and not four, and why color is what tells them apart: secondary is "nothing here yet", danger is "something went wrong", success is "you are done". It draws no surface: an empty state is always inside a card, a table or a panel, and a sheet inside a sheet is two sheets — PlTable's empty takes a widget, and this is the widget it was waiting for. The part worth getting right is the way out, and it is the reason actions is a slot rather than an afterthought: a screen that says "No projects" and stops is a dead end. actions is a List<Widget> here rather than the React build's single slot, because a Dart caller has no fragment to put two buttons in.

  • PlStat. One figure, and what has happened to it. A row of these is the top of every dashboard, and the whole of what makes them worth a widget rather than three Texts is the change: a number on its own says what things are, and a number with a movement beside it says whether that is going anywhere. The colour of that movement is decided by improvesWhen and not by the sign, which is the one thing a naive version gets wrong — churn going up is not good news, and a green arrow on it is a dashboard lying to somebody. value takes a widget rather than a number, deliberately and twice over: how a figure is written is the screen's decision, and this package has no dependencies to write it with — package:intl's NumberFormat is the app's. The one number the widget does write is PlStat.formatChange, at most one decimal and a sign on a rise. The figure is Flexible and ellipsises rather than overflowing, because a figure long enough to fill its card should truncate rather than paint a yellow-and-black bar across the dashboard.

  • PlBackTop. The way back up, once there is a way back up to want. It is hidden until it is useful, and that is the whole design rather than a nicety: a button pinned to the corner of every screen from the first frame is one more thing covering the content, and on a screen short enough not to scroll it is a control that does nothing. While it is out of reach it is out of the semantics tree and out of the focus order rather than merely faded — a control a reader can reach and cannot see is worse than one that is not there. controller is a ScrollController, or the PrimaryScrollController when it is left out, which is what a ListView with no controller of its own attaches to and is therefore this framework's "the window". The scroll is animated and not when the platform has asked for less movement: a screen that flies past a reader who turned animations off is the exact case that setting exists for. There is no floating and no equivalent of the web's position: fixed — where the button goes is the caller's Stack.

  • PlConfirmProvider and PlConfirmProvider.of. await PlConfirmProvider.of(context).confirm(…) completes with the answer, so the branch after a question stays in the callback that asked it. The argument for a lookup rather than a widget in the tree is the one PlToastProvider made: what a caller has at the moment a question is warranted is a callback, not a place in the tree — and without this, adding a confirmation to one button means a piece of state, a PlModal kept in the tree beside it, and the work after the answer torn in half across a callback. alert is the one-button form.

    Three decisions worth stating, all shared with the React build. Cancel holds the focus by default: a confirm dialog exists to make somebody stop, and an Enter key that lands on the destructive action defeats the whole thing. Questions asked while one is open are queued, in order, with the sheet's content changing rather than the dialog closing and reopening; a provider that unmounts with questions outstanding completes them all false, because a future that is never completed is a callback that never runs its finally. And PlConfirmProvider.of asserts outside a provider rather than answering false, because a silent false is a delete button that quietly does nothing.

  • PlPopconfirm. A question asked where it was raised. The difference from PlConfirmProvider is not the words, it is how much it interrupts — a modal takes the screen away and is right for the question that deserves that, and this appears against the row's own delete button and leaves the rest of the list readable. The rule of thumb is what happens if they answer by accident: if they can undo it, this is the one. color defaults to danger here and to primary on a PlButton, and that is not an inconsistency — nobody reaches for a popconfirm to ask whether to save. onConfirm returns a FutureOr<void>, which is the Dart shape of "a promise is waited for": the button shows its loading state until it settles, the popup closes only if it completes, and a stray press is ignored while it runs. An error is caught and goes no further — keeping the question up is the whole of what the widget owes a failure. The focus lands on the confirming button, the other way round from the modal: a popconfirm is opened by the button it is asking about.

  • PlassTheme sets the defaults once. Seventy-seven widgets resolved size, color and density from a literal in their own constructor, which meant an application whose controls are sm said so at every call site — not a design decision, transcription. Those parameters are nullable now and resolve from the nearest theme instead, in four layers and in the order a reader would guess: the widget's own parameter, then whatever set it is in, then the theme, then the widget's own default. So a size: PlassSize.lg on one button still wins inside a PlButtonGroup inside a compact application. The date vocabulary comes with it — names, labels and weekStartsOn reach all five date widgets, which is what a locale is here, since the framework ships no Intl.

    Use PlassTheme.merge. The plain constructors replace the defaults in scope; merge keeps the ones above and the brightness with them, which is DefaultTextStyle's own bargain and is there for its reason — an InheritedWidget has no context to read an ancestor with, so merging happens where there is one.

    variant and elevation are deliberately not in PlassDefaults, and the absence is the load-bearing part. variant names what a surface is made of, and a PlButton is solid while a PlCard is glass because that is the arrangement rather than an unset default; elevation is per-widget semantics for the same reason, a control resting on the sheet at 1 and a field cut into it at 0. One value for either would not be a default, it would be a flattening.

    A new package test asserts the contract from both ends — that a theme produces an identical subtree to writing the parameter, on thirteen widgets, and that no constructor anywhere under lib/ still bakes a style axis in. Two bugs the refactor surfaced were fixed with it: PlAlert decided whether it was a live region by reading its unresolved colour, so an alert whose danger came from a theme announced itself as though it were a note; and PlSidebar touched a lazily-initialised width from dispose, which is an inherited-widget lookup on a deactivated element.

Fixed #

  • A PlPill could not stand anywhere it was not given a width, which included the arrangement its own documentation describes. Row, and a Positioned that named only its top and its start — how a lozenge that floats over a screen is placed — both hand a child an unbounded main axis, and the pill's outer column was CrossAxisAlignment.stretch. Stretching against an unbounded constraint asks for a tight infinite width, so instead of a wide pill you got BoxConstraints forces an infinite width and a cascade of RenderBox was not laid out behind it — an error naming a RenderConstrainedBox and nothing you wrote.

    The stretch was right and its reason has not changed: the row and the details panel under it are one width, so the panel spans the pill rather than sitting in the middle of it. What was missing is that there is not always a width to stretch to. The constraints are asked first now, and where there is nothing to fill the pill takes the width of its own widest part and centres the rest on it — so a pill with an open panel is still exactly as wide as the wider of the two, which is what stretch was doing all along.

    Nothing changes for a pill that already had a width: offered a bounded one it still spans it, and a loose constraint still counts, so a Wrap of pills is still a column of them. That last one is now written on the page, because it is the thing that sends somebody to a pill when they wanted a PlChip. This is a Flutter-only bug: the React pill is inline-flex and is as wide as its content wherever it is put, which is a difference the component page now states rather than leaving to be discovered.

  • Seven widgets laid themselves out on the wrong side of a right-to-left screen. The package is written in EdgeInsetsDirectional and PositionedDirectional almost everywhere, and "almost" is the failure: a physical left: is invisible in review, correct in the only direction anybody develops in, and wrong for a reader who would never file a bug in English. Found by writing the test first, and every one fixed in the widget rather than in the test.

    PlSlider was the worst of them, because it was wrong in three ways that have to agree: the run was painted from the physical left, a press was read as a distance from the left edge, and the left/right arrow keys counted up regardless — so an Arabic slider showed its minimum at the wrong end, jumped to the opposite value when pressed, and walked away from the arrow. PlChatBubble's tail stayed on the physical right while the bubble moved to the left, so the corner pointed away from its own sender. PlPill put the air meant for the leading glyph on the trailing side. PlAnimateMarquee ran the strip off the wrong edge, delivering the words backwards. PlScrollZone measured a horizontal strip's groups from the left edge and snapped to the wrong ones. PlNavigationMenu's vertical panel always flew out to the right, which on a right-to-left page is off the screen. And a date range's opening and closing corners — shared by PlDateRangePicker and PlCalendar — rounded the wrong ends of the run.

    A new package test now asserts the contract in two halves. The first drives a real Directionality(textDirection: TextDirection.rtl) tree for the behaviours that must turn over; the second reads every file under lib/ and fails on a named argument that names a side of the screen, against a short list of ten deliberate exceptions that each carries its reason — a measured offset, a corner that has to be handed to a painter already resolved, or a PlassSide, which names an edge of the screen on purpose. That second half is the one that catches the next widget. Nothing here changes the public surface, so the compiled size is unmoved.

    Four more places were rewritten as directional widgets without a visible change, because a rule with exceptions nobody can enumerate is not a rule: PlSidebar's inner rule, PlTable's pinned header, PlAnimateLighting's glow, and the horizontal padding in PlAccordion, PlCollapsible, PlSpoiler and PlCommandPalette.

  • A chord ending in Space or a numpad Enter now matches. The matcher folded the framework's key label by stripping its spaces, which turns the space bar's own label — a single space — into an empty string, so 'Space' could never match anything; and Numpad Enter folded to a name no cap is written with. A one-character label is now left alone and everything else goes through the alias table, so both reach the key printed on the cap. Reachable only through the new hotKeys maps, so nothing that already shipped behaved differently.

  • Switching a calendar's view no longer overflows the panel for a frame. The three grids' cells were matched by position across a view change, so each one's AnimatedContainer tweened from the grid it came from to the grid it was now in — and three month cells standing beside a fourth year cell is wider than the seven-cell panel holding them. Dropping from the month grid down to the days was the same thing four cells over. The grids are keyed now, so a view change builds fresh cells already at the right width.

1.1.0 - 2026-08-30 #

Added #

  • PlAvatarGroup. A stack of avatars, overlapping, with the ones that did not fit as a +n. size, shape, variant, color and elevation are set once on the group rather than on every avatar — a stack whose fourth face is a size out is not a stack — and an avatar's own value still wins, which is what marks one of them out from the rest. It reaches a face through an inherited widget, internal/avatar_group.dart following internal/button_group.dart one component over, which is also why PlAvatar's five axes are now nullable: null is "this avatar did not say", the stack answers next and the house default last. Two things are said differently. The faces are a List<PlAvatar> rather than widgets, because the stack has to know how wide a face is to lay the next one over it. And there is no negative marginEdgeInsets asserts it is non-negative — so the overlap is a Stack of faces each pushed one step further along; the stack takes the size of its widest child, so the row still measures exactly as wide as it draws, and the step is arithmetic off the group's own size rather than layout. The ring is the same idea as the stylesheet's and the opposite construction: a Flutter Border is drawn inside the box it is on, so the gap between two faces is a plate two pixels larger behind each one, filled with the page's own surface. It reads as the hole the near face is cut out of rather than as a line around anything, which is what keeps two circles of similar tone from smearing into one shape. The avatar page's hand-rolled Stack goes with it.
  • PlMenubar. The strip of words at the top of an application — File, Edit, View — each of which opens the same menu a PlMenu does, with a different trigger. What makes it a bar rather than a row of menus is that it is one SemanticsRole.menuBar whose words are menuItems, that the axes are set once on the strip and hold for every menu on it, and that the strip sits a rung below the control ladder at every step and takes the compact padding track even at the default density: a menu bar is a strip of words, usually inside something that already has a height, and sized as controls File Edit View would be three buttons in a row. Two things are said differently. The menus are data rather than composed children, which is the shape every list-shaped widget in this package takes. And crossing the strip does not walk through the menus: an open menu's dismiss layer sits between the pointer and the words, so they never hear it arrive — pressing elsewhere puts the open one away, and only ever one is open either way. That is also why the open one is marked in colour as well as in the semantics tree, since once a menu is open it is the accessibility tree.
  • PlColorPicker. A colour chosen by eye: a saturation square with a hue rail beside it, the arrangement every design tool has settled on because it puts every colour of a hue within a single movement of the pointer. The decision the whole widget turns on is that the panel's state is HSV and it never leaves that model — through RGB every shade of black is the same colour, so a picker that re-parsed its own output would snap the hue rail to red the moment the pointer reached the bottom of the square. An incoming value re-seeds the model only when it means something different, compared as a colour rather than as a string. The conversions are a new internal/color.dart, a hundred lines of arithmetic with no trigonometry in it, which is the entire reason a widget that computes colours brings nothing with it — in a package whose whole claim is that it has no dependencies. The tick on a chosen swatch is black or white by relative luminance, because a fixed white tick disappears on yellow, and the default swatches are a plain spectrum plus the greys rather than the package's six families: those are semantic roles, and a picker is asked for a colour rather than for a meaning.
  • PlCommandPalette. Everything an app can do, behind one field. It is not a PlMenu — a menu is a short list in one place and every row is visible before you look for it — and not a PlCombobox: what comes back is not a value, it is something happening. Filtering happens before the list so a group heading can be drawn from the same array the rows come out of, and keywords are matched and never drawn. Three things are said differently. open is required: there is no uncontrolled mode, because what opens a palette is a key bound on the whole app and an app that binds one already holds the state. The four list keys — the arrows, Enter and Escape — are read in the palette's own keyboard handler before the focus system, because the field has the focus and an EditableText consumes those keys itself; reading them first is the only way the field keeps every character while the list keeps its four. And the fold is case only, for the reason PlTransfer's is: Dart's core has no String.normalize. The opener is read with the same Mod-aware vocabulary PlHotKeys draws, so the cap on a row and the key that works cannot drift apart — and a row's own shortcut is displayed and never bound, because the app has already bound it.
  • PlFieldset. A group of controls that answer one question together, with a name on it, drawing no surface — a grouping is not a sheet, and the sheet already exists. What it owns is the legend, the gap the controls stand at, and disabled. That last one is the one thing said differently: on the web a <fieldset disabled> is an attribute the browser applies to every control inside, including one a widget three levels down built and never heard of, and there is no such cascade here. So it does the three things that cascade actually buys — the pointer taken away, the focus taken away, and the group drained the way every disabled surface in the package is. What it cannot do is make a field inside report itself as unavailable, which needs every widget to agree to look; a field that has to say so is given its own disabled.
  • PlForm. A form that knows which of its fields is wrong: the stack, a submit that collects every FormField's validity at once, and errors — an answer from outside the app's own validation, keyed by the name of the field it belongs to. It is not a form library, and it draws no surface. Two things are said differently from the React build and both are the same fact: there is no native form here. On the web a field's name puts it in the submission and its constraint validation is the browser's, so the form can collect values and route messages on its own; here a field is a widget holding a controller the caller already made. So onSubmit reports that the form is valid rather than a map of values, and errors is read explicitly through PlFormScope.errorFor(name) — the one piece of wiring this build asks for, and the reason that scope is exported rather than internal. submit() is reachable from a GlobalKey<PlFormState> or from anything inside through PlFormScope.maybeOf, because there is no native submit for a button to trigger.
  • PlNavigationMenu. A site's navigation: a row of destinations, some of which open a panel of more of them, claiming SemanticsRole.navigation and announcing a destination as a link and a panel opener as something that expands. The row carries no surface at rest — the words are the screen's own, and five bordered boxes across the top of an app is a toolbar rather than a navigation. Three things are said differently. There is no href anywhere, because there is no navigator in this package and no address to resolve: onPressed is where a destination is decided, exactly as on PlTextLink. The items are data rather than composed children, because the row has to know which item is which to keep one panel open at a time. And there is no controlled mode: String? cannot tell "the caller did not say" from "the caller says closed", so a controlled value would be one that could never be closed from outside — which panel is open is the pointer's state rather than the app's, and initialValue is all it needs. Each item anchors its own popup, so crossing the row swaps panels rather than growing one; the resize the React build does is Base UI measuring both panels, and it has no counterpart here.
  • PlTransfer. Two lists and the arrows between them, for a choice that is long — the columns in a report, the permissions on a role, the people on a channel. The model is one sentence, and it is the same on both sides: ticking is not choosing. The value is which side a row is on, the ticks are which rows the next press will move, and keeping them apart is what makes a press a deliberate act rather than a side effect of reading. Two things are said differently. A row's label is a String rather than a widget, because the filter reads it and a row the filter cannot read is a row that disappears from a search it could never satisfy. And the fold is case only: the React build strips combining marks as well, so cafe finds Café, and Dart's core has no String.normalize — pulling a package in so that a search box folds accents would put a dependency in every consumer's binary for the sake of one comparison, in a package whose whole claim is that it has none.
  • PlToggle and PlToggleGroup. A button that stays down, and a set of them that share one state. The rule the whole widget is built around is that off is neutral: a PlButton at rest is an action waiting to be taken, and a toggle at rest is a state that is currently false, so accent ink on an unpressed toggle would be saying it was on. On, the three materials give the same two answers a PlSegmentedButton's chosen segment gives, and the elevation does not move with the state — "on" is a fact about the thing beside the toggle rather than about how far the key is off the screen. The group squares off the corners facing a neighbour by publishing the same PlassButtonGroupScope a PlButtonGroup does, so a toggle picks either kind of run up by one route, and reports a list in both the single and the multiple case. One thing is missing on purpose: there is no roving focus, so each toggle in a group is its own focus stop. Base UI's roving tab index has no counterpart in widgets.dart, and a roving focus implemented badly is worse for a keyboard reader than the platform's own traversal — PlSegmentedButton carries the real thing where a value needs it.
  • PlSidebar, and the PlSidebarTrigger that brings it back. A column beside the screen's content, and a drawer once the screen is too narrow to hold one — two presentations of one panel, so a caller never swaps widgets at a breakpoint and the child is built once either way. It claims SemanticsRole.complementary and is named by default, because Flutter refuses a duplicated landmark with no label outright. Four things are said differently. The layout's collapse answer is measured against its own width rather than the window's, because a LayoutBuilder sees the constraints it was handed; giving the sidebar its own collapseBelow is what asks for the window instead. The trigger is not built at all while the sidebar is a column, rather than hidden by a media query — that class exists on the web to keep the button in the markup a server sends, and there is no first paint to hold together here. A collapsed panel with no title draws its semanticLabel as the heading, because a PlDrawer is named by what it draws. And the resize handle is a Semantics(slider: true) with the width as its value rather than a role="separator" with an aria-valuenow, since the semantics tree has neither: a handle is what it actually is to a screen reader, a control with a value that can be turned up and down. The dragged width lives in a ValueNotifier rather than in setState, which is the Dart spelling of the same decision the React build makes by writing it straight onto the element — nothing but one box depends on the number, and rebuilding on every pointer move would rebuild every row in the panel.
  • PlFooter. The sheet at the end of a screen, and the one component in the package that claims SemanticsRole.contentInfo — the landmark a screen reader offers as "the app's own information" rather than "more of the content". It claims it unconditionally, which is the one thing said differently: on the web the <footer> tag is only promoted to contentinfo at the top level of the document, and a widget has no top level of a document to be promoted at. It has no slots on purpose, which is the whole difference between it and PlHeader: a header's three regions are a fixed arrangement worth writing once, and a footer's content is four columns on one screen and one line on the next. What it decides is the sheet — the surface, the gutter, the measure, and the hairline along the top edge, which is the one that faces content.
  • PlHeader. The bar across the top of a screen: a brand at one end, the actions at the other, and whatever belongs in the middle. Its three slots are parameters rather than sub-widgets, for PlCard's reason — the arrangement is fixed and what a caller decides is what goes in each — and that the middle can be centred on the bar's own midline is only possible because the ends are the widget's to measure: both ends take an equal share by construction, so a logo one character longer does not move the navigation. size is a floor and not a height (md is 64, a 40 control with 12 either side), so a bar whose content wraps grows and keeps its padding. Three things are said differently from the React build. brand and actions are lists, because a slot is a row and the gap inside it is the bar's to decide — the same shape PlToolbar gives its two ends. There is no position: a fixed element has to span something, and a widget goes exactly where the screen puts it. And there is no banner landmark to claim, so a named bar is a SemanticsRole.region and an unnamed one claims nothing at all — Flutter refuses a region with no label, on the grounds that a landmark nobody can name is a landmark nobody can skip to.
  • PlPageLayout. The skeleton a screen is hung on: a header, a footer, one sidebar or two, and the content between them. It draws no surface, claims nothing but SemanticsRole.main around what it was given, and leaves the gutter and the measure to PlContainer — a layout that did both would be a second spelling of one idea. Two things are said differently from the React build, and both are the framework doing the work. There is nothing to measure: React has to write the header's height onto the root because a sticky bar sits across the top of the window without taking anything out of the flow, and a Column has already done that arithmetic — the band below the header is exactly what the header left. And collapseBelow is checked against this layout's own width rather than the window's, because a LayoutBuilder sees the constraints the layout was handed: an app shell inside a pane collapses when the pane is narrow, which a media query cannot say. null is the Dart spelling of "no floor was named". There is no scroll and no height — the layout fills the space it is given and what scrolls is whatever is put in child — and no skip link, because a link to a fragment needs fragments and a traversal order that is the document's.
  • PlTextLink takes a startIcon. Its one icon slot was the mark after the label, and it is not a general one: icon is about the link's destination and has an opinion, drawing the arrow for an external link unless showIcon says otherwise. There was nowhere to put the other kind — a favicon, a file type, a lock — so a caller built their own Row and paid for the gap by hand. startIcon is the plain widget the rest of the package spells the same way: nothing is drawn unless something is put there, and it rides at the link's own mark size a quarter-em from the label, the same as the one on the other side.
  • A PlScrollZone answers the wheel. A horizontal Scrollable reads the horizontal half of a scroll and a mouse wheel only ever produces the vertical one, so a shelf under the pointer sat still while whatever was behind it moved instead. wheel is on by default and takes only the vertical half of a gesture, and only while the strip has somewhere left to go: a trackpad's two fingers and a tilt wheel already scroll it sideways and are left to the framework, and the moment the strip reaches an end the wheel goes back to whatever is behind it — a shelf that swallowed it at both ends would be a hole a reader scrolls into. It goes through the PointerSignalResolver rather than acting where the event arrives, because a scroll view that has claimed the same event is deeper in the hit test and registers first, which is what keeps the two of them from both moving the strip.

Changed #

  • PlAvatar's shape, variant, size, color and elevation are nullable. Each of them used to carry its default on the parameter; now the default is applied when the value is resolved, and null means this avatar did not say — which is what lets a PlAvatarGroup answer for a face that stated nothing while a face that stated something keeps it. The arrangement PlButton already has inside a PlButtonGroup. Every call site that passed a value is unaffected; one that read the field back now reads a nullable.
  • A PlAccordionItem's title and subtitle wrap. They were each held to one line and ellipsed, which is the right rule for a table cell and the wrong one for a fold: an accordion is most often a list of questions, and a question is a sentence. A heading that ran past the header lost its end with nothing to press for the rest — a fold opens its body, not its heading — and the narrower the screen, the more of the question went. What wrapping costs is a header two lines tall, in the one component whose entire job is to change height. truncate puts both back on one line for the header the old behaviour was right for: a name out of a database sitting beside a control.
  • A PlScrollZone's buttons stand beside the strip rather than over it. buttonPlacement now defaults to PlScrollZoneButtonPlacement.inline. An item sliding under a button reads as something being hidden far more often than it reads as depth, and the two scroll buttons are the one thing on a shelf that must never be mistaken for part of what is on it — so the scroller stops where the button starts and an item is cut off at its edge instead. overlay is unchanged and is still what a shelf of pictures wants.

Documentation #

  • The README no longer calls the package a preview. It said a component's parameters and defaults were "settled enough to read" and what it would look like after real use was not — true of a package that had not reached 1.0.0, and no longer true once it did. In its place is the thing a reader of one half actually needs to know about the other: both packages hold the same components under the same parameter vocabulary and the same tokens, and they version independently, so this package's number and the npm one's will not always agree. Nothing under lib/ changed.
  • The README opens on the design rather than on the word "material". It led with "a component library with a material rather than a theme", meaning material as the stuff a surface is made of — which is not the reading a Flutter developer reaches for first, in a package that goes out of its way to import neither material.dart nor cupertino.dart. One paragraph describing the design itself takes its place, shared word for word with the React package and the repository's own README, and the points under it are written from the caller's side. Importing neither library is still said, in the bullet about bringing nothing in behind it, where it is a benefit rather than a headline.

1.0.0 - 2026-08-29 #

The rest of the library. PlButton shipped first and everything else follows it, ported from the React package the same way it was — value for value, with the places where "the same number" would have been wrong written down where the conversion happens.

Added #

  • PlAnimateTyping. Text appearing one character at a time, and it costs both kinds of reader nothing. The whole string is the widget's accessible label while the drawn copy sits behind ExcludeSemantics, so a screen reader is given the text once rather than being made to sit through the performance. And the box the whole string will need is held from the first frame by an invisible copy underneath, so nothing around it is laid out again as the characters arrive — Flutter lays a Text out from what it holds, so the space has to be reserved by something holding the whole string. The advance is by grapheme and not by code point: String.characters knows where the boundaries are, and a typewriter that advanced by code points would spend four frames assembling an emoji out of parts that mean nothing on their own. The text is one positional String rather than a widget or a list of children, because there is nothing to flatten: a typewriter's input is text.
  • PlAnimateHeadline. One line replacing the one above it, on a timer. Every line sits in the same cell — a Stack, which is as tall as its tallest child — so the box never resizes as the reel turns, and the lines that are not showing are drawn at zero opacity rather than taken out of the layout. It is deliberately not a ticker: interval is counted from the moment a line arrives rather than from the start of the cycle, so raising duration does not quietly eat the reading time. Passing index makes it controlled and takes the timer away outright — a controlled headline is somebody else's clock. rise is a double? where null is one line's own height, the same trade PlAnimateSlide's distance makes; there is no alternate, because a reel has no other direction to run in.
  • PlAnimateMarquee. Content scrolling steadily past, forever, with the copies laid end to end so the moment the first has left the second is standing precisely where it began. Three things are said differently. The strip is longer than its box by design, so it is laid out inside an UnconstrainedBox with Clip.hardEdge rather than a plain clip — a clip alone clips the paint and leaves the RenderFlex asserting that it overflowed. There is no percentage translate to resolve against the element's own box, so the strip is measured and moved by that many pixels, and the measurement decides the duration as well: speed is logical pixels per second, so a strip of four logos and a strip of forty move at a reader's pace instead of the long one becoming a blur. And where an entrance's reduced-motion answer is its finished frame, a marquee's is t held at zero — the content standing where it started. PlassAnimateRun learned to restart a simulation in flight when its duration changes, which is the case a marquee lives in: the strip is measured after the first frame, so the pass already running is the one whose duration has just become correct.
  • PlAnimateLighting. A light travelling around the outside of something, and the only way this package has of saying here without also saying and it moved. The light is a SweepGradient on a layer behind the child — first in a Stack with clipBehavior: Clip.none, inset by a negative spread — so it reaches past the content and still sits under it, and nothing inside is altered or overlaid. The arc turns between the two ends of the family as it travels, which is the rule every filled surface follows: a flat coloured arc would be the one place in the package where a coloured surface is paint. What moves is the gradient's own GradientRotation rather than the layer's, for the same reason the CSS animates a conic gradient's angle rather than rotating the pseudo-element: turning the layer swings its corners out past the content on every quarter turn. size has to agree with the radius of what is inside it.
  • PlAnimateAppear. A list of things settling into place one after another, and the effect belongs to the set rather than to any one item. Two things are said differently. The trigger is one gate above every child rather than one per child, so a visible list is a list that starts together and then staggers, not eleven widgets each deciding for itself when it is on screen; each child is then an ordinary run held back by its own delay. And it lays its children out, which the React build does not have to: there is no stylesheet here to put a display: flex on the container, so orientation and spacing are what a className would have done, and anything more elaborate belongs inside one child — which also makes that whole arrangement one step of the stagger. paused goes to the children rather than to the gate, so holding the set still holds it where it is instead of rewinding it.
  • PlAnimateBlink. Content pulsing between full opacity and a floor, on a symmetric cycle — full, faint, full — so however many times it runs it ends where it started. A blink that finished halfway would leave the widget permanently half drawn, which reads as a rendering fault rather than as an effect that ended. repeat is left at null here, the value that never stops, because a single blink is a flicker and nobody asks for a flicker. min is the floor, and it is worth raising for anything that has to stay readable while it pulses — and it is never the only thing carrying the message, because a platform with animations turned off shows none of it.
  • PlAnimateRotate. Content turning about a point, with two angles rather than onefrom alone is an arrival, and from: 0, to: 360 with a null repeat and Curves.linear is a spin that never lands. One thing is said differently and it is deliberate: both angles are degrees, not radians. The framework counts in radians and the design language counts in degrees — every gradient in the package is at 135° — so the conversion happens inside the widget, once, rather than at every call site. It is not for text: a rotated word is resampled along its whole length, which is exactly what the rule against transforming a control exists to prevent.
  • PlAnimateSlide. Content travelling in from one edge, and with no distance it travels the widget's own size — so it starts exactly out of frame and arrives without ever having been half drawn somewhere it does not belong. That is the one thing said differently here: distance is a double? in logical pixels and null means its own size, because a fraction of a widget's own size is what FractionalTranslation already means and there is no CSS length to spell. from is physical — PlassSide.top, .right, .bottom, .left — as it is everywhere in the package. Nothing around it is laid out again while it runs.
  • PlAnimateZoom. Content arriving from the middle of where it will end up — the same arithmetic as PlAnimateGrow at more than twice the distance, and always about the centre. That is the whole difference: a grow unfolds from somewhere, a zoom comes at you, which makes it the effect for the one thing on a screen meant to interrupt. There is deliberately no origin: a zoom anchored to a corner is a grow, and the package does not offer two spellings of one idea.
  • PlAnimateGrow. Content unfolding from a point. What separates it from a zoom is origin and how far it travels: a grow starts close to its final size and can be anchored to any edge, so it reads as something opening out of the thing beside it. origin is an Alignment rather than a CSS transform-origin string — the framework already has the type, and Alignment.topCenter reads better than 'top'. One other thing is said differently: with fade off there is no Opacity widget at all rather than one sitting at 1, so nothing in the tree claims to be doing something it is not and there is one fewer layer to composite.
  • PlAnimateFade, and the machinery the whole PlAnimate* set runs on. Content arriving or leaving on opacity alone — the plainest effect there is, and the one that suits the material: a Plass sheet is defined by what shows through it. Underneath it, internal/animate.dart is the Dart half of the React package's internal/animate.ts and keeps the same two ideas. Waiting is a held first frame, not a hidden widget: a visible fade sits at t = 0, faded out and taking its space, until it is scrolled into view — which is what fill-mode: both plus a paused play-state buys over there, and the reason an untriggered effect never flashes its finished state first. And a run that has finished stays where it ended, so an exit fade stays faded out. Four things had to be said differently. duration and delay are Durations rather than milliseconds, because the framework already has the type. easing is curve, a Curve. repeat is an int? where null never stops — there is no 'infinite' to write, and -1 would be a sentinel a caller has to look up; it is the same trade PlProgressLinear makes with a null value. And mode is PlassAnimateMode.enter / .exit, because in is a reserved word in Dart. trigger: visible watches the nearest Scrollable instead of an IntersectionObserver, and with no scrollable above it there is nothing to watch, so it runs — exactly as the React build does when the browser has none.
  • A test that keeps the package off Material. Nothing under lib/ may import package:flutter/material.dart, package:flutter/cupertino.dart or dart:mirrors, and test/package/framework_surface_test.dart fails if one ever does. Dart's tree shaker is whole-program and very good — the whole gallery, all 63 components, compiles to about 158 kB gzipped more than an empty Flutter app, and a light component costs single-digit kilobytes — but a MaterialApp reaches ThemeData, its typography and its ink machinery, and measures about 127 kB gzipped on its own. That is one import away from every consumer, it would pass every widget test, and no review would catch it. tool/size.dart is the on-demand measurement the numbers come from.
  • PlDateTimePicker. A day and a time in one popup — the calendar is PlDatePicker's and the columns are PlTimePicker's, both unchanged, and they sit at exactly the same height because both read the same cell ladder: seven rows each, so the popup is one rectangle rather than two of different sizes pushed together. The bounds do more work here than anywhere else and it is the one place this parts company with PlDatePicker: minDate is read at full precision, so a minimum of 09:30 on the 27th leaves the 27th selectable in the calendar and greys out the morning in the clock — what a "not before now" rule needs, and what a day-granular check cannot give. Picking a day leaves the clock alone and picking an hour leaves the day alone, because nobody reads a popup in the order it was written. The trigger wears the calendar glyph alone: a control cannot say two things at once.
  • PlTimePicker. A time of day, chosen from columns rather than a dial, with the bounds checked at the granularity of the column being drawn: with a minTime of 09:30 the hour 9 stays available — it contains allowed minutes — and the minute column is where 00 through 25 grey out. Two things are said differently. hour12 is a plain false rather than "whatever the locale does", because there is no Intl here to ask, and the words it uses when it is on are the PlDateNames.am / .pm that already come with the month names. And the chosen row is brought into view by computing an offset and jumping its own ScrollController, never ensureVisible, which walks every scrollable ancestor and would move the screen behind a popup that has only just been positioned. closeOnSelect is false — a time is two answers — so the footer carries a Done.
  • PlDateRangePicker. A span between two days: PlDatePicker with a second end, two months side by side as one calendar in two halves, and the band drawn between the anchor and the pointer before the second press lands — that preview is the whole affordance. Pressing backwards is the same range said in the other order. Two things are said differently here. value is a non-nullable PlDateRange with a PlDateRange.empty for nothing chosen, because a value that cannot be null is one fewer thing for a caller to guard. And a preset's range is always a callback rather than sometimes a value: a preset almost always depends on today, and "the last 7 days" computed once at startup is wrong for anyone who left the app open overnight. The popup needs an IntrinsicHeight around its row and an IntrinsicWidth around its presets — a portal hands its popup loose constraints, and a divider that stretches inside those is a divider asked to be infinitely tall.
  • PlDatePicker. One day, chosen from a calendar, on a PlTextField's shell wearing a calendar glyph. The header is what it is for: the month name and the year are each a button that opens a grid of its own, so any month of the year on screen is two presses and any year at all is three, and all three views share a width and a height so switching never resizes the popup. Six weeks always. This is also the one place the two packages genuinely part company: React gets Intl from the browser, so a BCP 47 locale string produces every month name and the order the header writes them in — the framework ships nothing of the kind, and a package that pulled package:intl in to fill the gap would be making a dependency decision on its consumer's behalf. So the words arrive as a PlDateNames: English by default, so a picker works with no setup at all, and three lines of DateFormat for an app that already has intl. format became formatValue, the same trade the indicators already make. The header's own controls are one step down the size ladder and both of its buttons truncate rather than overflow, because a month name is July in one language and септември in the next and the row has seven cells to fit inside.
  • PlCombobox. A field you can type into and also choose from, on the same anchored layer a tooltip and a select's list stand on. The shell is a PlTextField's to the pixel, exactly as PlSelect's trigger is. Two things had to be said differently. Holding a set is a second constructor, PlCombobox.multiple, rather than a multiple flag: one widget with a flag would have to hold both shapes of value and neither would be typed. And allowCustom became onCreate, a T Function(String) — React can default that permission to true because a value there is always a string or a number, and here it is a T that only the caller knows how to build, so the permission and the recipe are the same parameter. Nothing is committed when focus leaves: the query goes back to being the value, and a value the list does not have is only ever taken by taking its row.
  • PlProgressBox. A row of small glass plates that light up, and the shape that is about the material rather than the quantity. With a value the plates fill in order and the leading one partially, so four plates at 30% are one full plate and a fifth of the next rather than a quarter rounded off; each plate is a groove of its own for exactly that reason. Indeterminate they cycle, each held back by its own index, and what cycles is the fill's opacity — the gradient is there the whole time at full strength, because a gradient has nothing to interpolate toward when it is absent. The plates never move. One ticker per row rather than one per plate: the plates read the same controller at their own phase.
  • PlProgressCircular. A ring that fills, for where there is no room for a bar. It takes PlProgressLinear's parameters unchanged except for size, which is diameter here rather than thickness. The arc is the family's gradient rather than a flat colour, which is the one place in the package a Shader is built by hand: a stroke takes a shader rather than a decoration, so the 135° sweep every other surface gets from PlassCssGradient is asked for directly. Indeterminate, it draws a fixed quarter-arc and turns — the same exception a spinner already has — and slows rather than stopping under MediaQuery.disableAnimations. The value and the label sit beside it, because a number in the middle of a fourteen-pixel dial has nowhere to go.
  • PlProgressLinear. A bar that fills, and the one indicator that shows how much is left at a glance. The groove is the same neutral ink a PlSlider's rail and a PlSwitch's off track are cut in, and the run over it is the family's gradient — the material the button that submits the form is made of. Two things had to be said differently. format is a function rather than an options object: there is no Intl.NumberFormat in the framework to hand options to, and a package that pulled package:intl in to provide one would be making a dependency decision on its consumer's behalf. And the indeterminate segment travels on a directional Alignment rather than on inset-inline-start — neither is a transform, and both run the other way under RTL without being told. It is one merged semantics node carrying SemanticsRole.progressBar, or loadingSpinner and no value at all when it is indeterminate.
  • PlButtonGroup. A run of buttons that belong together. The corners that face a neighbour are squared off and the six axes are stated once for the set, exactly as in the React build — but two things had to be said differently. children is a List<Widget> rather than one child, because the group has to know which member is at each end to decide which corners to square. And the seam is made by not drawing the hairline that faces a neighbour, rather than by pulling the key back a pixel so two lines overlap: Flutter has no negative margin — EdgeInsets asserts it is non-negative — and the alternative is a Transform, which this library does not put on a control. Both arrive at one hairline per seam. The axes on PlButton and PlIconButton became nullable to make any of this possible: Dart cannot tell a default apart from a value that was passed, so this button did not say has to be a value the type can hold.
  • PlToolbar. A bar of controls: an application header, a screen's action row, the strip along the bottom of an editor. Three slots and a row — start and end pinned to their ends and child taking what is left, which is the arrangement every toolbar has ever had, so it is laid out here rather than left to a caller and a spacer they have to remember. It takes no height: a toolbar is as tall as the controls in it plus the size / density padding every other surface uses. There is no position — a fixed element has to span something and a Flutter widget goes exactly where the screen puts it — so all that is left of it is rounded, turned off for a bar held against an edge, because a rounded corner there is a gap with nothing behind it. It claims no toolbar semantics either, deliberately: that role is a promise about keyboard behaviour, and a bar that claims it without implementing it is worse for a keyboard reader than one that never claimed anything.
  • PlSpoiler. Content that is covered until somebody asks for it, and the cover is a blur rather than a hidden box: a reader can see that there is something there and roughly how much of it, and cannot read it by accident. Blur alone is not cover, so a wash of the screen's own surface goes over the top — which also gives the button something to stand on. It is the one widget in the package that is happy uncontrolled, because what is being remembered is a thing the reader did to this box rather than a value the screen owns. While it is covered the content is behind ExcludeSemantics, ExcludeFocus and an IgnorePointer — the three things inert says over there.
  • PlPopover. A sheet that opens beside the thing that opened it, on PlassAnchoredPortal — the same layer a tooltip and a select's list stand on, so all three stay stuck to their anchor through a scroll for the same reason. trigger is required here where React makes it optional: a browser can position a popup against the viewport with no anchor, and a LayerLink has nothing to follow without one. There is no modal and no alignOffset — there is no page scroll to lock, and the anchoring is a flip rather than a slide.
  • PlPill. A floating lozenge holding a small amount of live information, and the one stadium in the library — the exception the radius rule is drawn against, because this is not a sheet lying on the screen but an object hovering over one. The corner is half the row's minimum height rather than half of whatever the pill has grown to, so a two-line pill keeps the corner it always had. There is no position: a fixed element has to span something to be centred in it, and a Flutter widget goes exactly where the screen puts it.
  • PlDrawer. A panel attached to one edge of the screen, and two things in one widget because they are the same panel: overlay opens over the screen on PlassPortal — the same layer a modal and an overlay stand on, so one opened over the other shows no seam — and inline is simply part of the layout. There is no variant and no elevation, because a panel that has taken an edge of the screen has already answered what those ask. There is no trigger either: the app opens it by setting open, and the button that does it is the app's own.
  • PlCollapsible. One section that folds, standing on its own — the same fold a PlAccordion is a set of, with nothing to coordinate with. The panel is a SizeTransition clipping a body that never changes size, so nothing is transformed and no text is resampled. trigger becomes triggerBuilder, which is forced: a React element can be cloned with new props and a Dart widget cannot be handed a tap handler after it was made. keepMounted has a sharper reason here than there — a Flutter State goes with its widget when it leaves the tree, so without it a folded-away field forgets what was typed into it.
  • PlCarousel. A strip of slides, one of which is in view, on a PageView — Flutter's own scrolling with snap points, so a swipe is the platform's and not a gesture handler imitating it. It gains aspectRatio, which the React build has no need of: a browser's strip is as tall as what is in it and a PageView lays every page out at the viewport's size. autoPlay is off by default, pauses under the pointer, does not start at all under reduced motion, and does nothing while the carousel is frozen.
  • PlBox. The plainest surface in the library: a sheet of glass with content on it and nothing else claimed. size is the size of the sheet — its radius and its padding — rather than a height or a type scale, which is the one place in either package where the word means something else. It gains one parameter the React build has no need of: clipped, because overflow: hidden is a class over there and a widget here, and a clip cuts anything a child draws outside itself.
  • PlScrollZone. A strip of anything laid out in one direction and scrolled in it, in as many lines as you ask for. The mechanism is an ordinary SingleChildScrollView and everything on top is a way of driving one; a group is measured off what was actually laid out rather than assumed, so a press moves one whole column and not a fraction of one. spacing is a length here rather than a step on the React build's spacing scale, controller is offered because Flutter drives a scroll view with one, and drag puts PointerDeviceKind.mouse back into dragDevices — which Flutter leaves out for the same reason a browser does, and which a shelf is exactly the place to ask for.
  • PlAspectRatio. A box that keeps a proportion and draws nothing at all. fit is nullable here and null by default, where React defaults it to cover: in a browser object-fit only ever reaches a replaced element, and a FittedBox reaches whatever it is handed — so a fit applied by default would scale a column of prose.
  • PlPanes. The one place Flutter makes the same idea easier rather than harder: the React build needs a ResizeObserver because CSS measures itself, and a LayoutBuilder is handed the extent on every layout pass. PlPaneSize is two constructors where React has a number | string union, a handle is slider semantics because Flutter's tree has no separator role and no valuenow, and none of the three drag hazards the other build has to work around exists here at all.
  • PlMenu. The one component whose shape differs between the two packages, and it is forced: React composes its rows because Base UI reads the DOM they are written into, and there is no tree to walk here — so the rows are a sealed PlMenuEntry hierarchy the menu is handed. Focus stays on the trigger while the popup is up, which is what PlSelect does; the arrows, Home, End, Esc and typeahead are bound there, and the pointer moves the same highlight the keys do. Submenus open on hover and on the arrow key that points at them, and the arrow follows the writing direction. There is no PlContextMenu: no right-click gesture means the same thing on every platform this package runs on.
  • PlFloatingBottomNavigation. The other half of the bar above, and a separate widget for the same reason it is in the React package: one is attached to the edge of the screen and the other is not part of it at all. There is no full-width strip to build here — a fixed element has to span something, and a Flutter widget goes exactly where it is put — so the bar is only as wide as its capsule. The key marking the current destination is one widget measured off whichever disc it belongs to and animated between them, the way PlSegmentedButton's tile is: a surface fading up on one disc while it faded out of another was two objects changing colour, where there is only ever one key and where it went is the whole of what the bar has to say.
  • PlBottomNavigation. Destinations as descriptions rather than widgets, and an item's label is a String rather than a widget: it is the name that is drawn and the name that is announced, and only a string can be both. There is no position — a Flutter screen has no page scroll to opt out of — and no href, so onChanged is where a router is called. The current destination is marked selected, which is the nearest thing to aria-current that does not claim the item is a toggle.
  • PlOtpField. One editor behind the whole row, drawn as slots — Flutter's text input is a single connection to the platform, and the React build's one-<input>-per-slot would be six keyboards fighting over one code. It is laid out over the row at zero opacity rather than taken off screen, because an input has to be measured to hold that connection. Rejected characters go through a formatter of the component's own rather than FilteringTextInputFormatter, so a refusal has somewhere to go instead of vanishing.
  • PlRating. The fraction is the filled star laid over the empty one and clipped by a CustomClipper, from the leading edge — so nothing is transformed, nothing is scaled, and it fills from the right under RTL without being told to. There is no radio group to inherit a keyboard from, so the arrows are bound on the row and every choosable fraction is a semantics node in a mutually exclusive set. readOnly drops the gestures entirely for one image node.
  • PlIconButton. A disc with a glyph in it, and almost none of it new code — everything is PlButton's. label is required for the reason it is in the React build: an icon button with no accessible name is the commonest accessibility defect a component library ships. PlButton gains one escape hatch, borderRadius, which is the door the React build already has through an inline style.
  • PlGrid and PlGridItem. A cell is a description rather than a widget, for the reason an accordion's folds are: the grid packs its members into rows by the columns they take, and a Widget is opaque. PlassResponsive<T> arrives with them, with the base value positional — Dart has no union type to carry "a number or a map", and the breakpoint is resolved against the window, which is what a CSS media query measures. wrap: false scrolls sideways rather than overflowing: a Flutter screen has no page-level overflow to leave the scrolling to.
  • PlContainer. The gutter and optionally a measure, drawing nothing. maxWidth is a nullable PlassSize where React has a 'none' in the union — Dart already has a word for "not set". The measure is the same ladder unit for unit, because a rem against a 16px root and a logical pixel are the same length.
  • PlTypography. The type scale on its own, so a page can use it without wrapping its prose in a card. level sets the size and whether the line is announced as a heading; there is no size beside it, because a size would let a caller ask for an h1 at xs. PlTypography.rich is the span form.
  • PlAvatar. A picture, or the initials, or a silhouette — never an empty box. image is an ImageProvider rather than a URL, which is the shape every image in Flutter has, and the fallback is shown until the first frame arrives and for good if none ever does.
  • PlBadge. content and count are two parameters rather than one, because max and showZero only mean anything for a number: the type is the question the React build has to ask at runtime.
  • PlChip. A pressable chip and its delete affordance are two separate focus stops, neither inside the other's gesture recogniser — the same shape the React build reaches for, there because a <button> inside a <button> is invalid and here because a nested recogniser would take one tap twice.
  • PlCard. The sheet, with the title, subtitle, header action, body and footer laid out on it. onPressed is what render={<a href>} was mostly reached for: it makes the card a real focus stop, announced and keyboard-reachable, and it lifts.
  • PlAlert. The three shapes, the six severities and their marks. Flutter has one live region rather than two politeness levels, so role="alert" against role="status" becomes whether the alert is a live region at all.
  • PlSlider. One thumb per value, so a range is the same widget with a longer list. There is no native range input to inherit a keyboard from, so the keys are bound here: arrows step, Page takes a tenth of the range, Home and End jump to the ends. The thumbs cannot cross.
  • PlSegmentedButton. The tile is measured off the chosen segment and animated as a rectangle — nothing is transformed, which is what lets the house no-transform rule survive a component whose whole point is that something moves. One focus stop for the set, arrow keys within it.
  • PlPagination. The same window arithmetic as the React build, down to the gap of exactly one page being filled with that page rather than an ellipsis, and every button in the row is a real PlButton. There is no getPageHref: nothing crawls a Flutter app, so the row is buttons and onPageChanged is where a router is called.
  • PlTextField. Single- or multi-line, with the label, the description and the error as part of the component. Built on EditableText rather than on a TextField: the latter is Material, and what Material adds on top — the decoration, the counter, the ripple — is what this component is instead of. The cost is named on the page: a selection has no drag handles, because those belong to Material and Cupertino.
  • PlCheckbox, PlRadioGroup and PlSwitch. Every one of them is controlled — handed a value, reporting what the value should become — which is how Flutter's own controls work and why there is no defaultChecked anywhere in the package. A radio group is generic in its option's type, so value and onChanged are checked rather than dynamic, and it owns the roving tab index: exactly one option is in the tab order, the rest are wrapped in an ExcludeFocus, and the arrow keys move the choice with wrapping.
  • PlTextLink. A line under a word, and no href: Flutter has no navigation of its own, so where a link goes is the app's and onPressed is where it is decided. Announced as a link rather than as a button, which is what puts it in a screen reader's list of links.
  • PlHotKeys and PlKbd. Mod resolves per platform from defaultTargetPlatform, and every key drawn as a glyph announces its real name — read out is "place of interest sign", which is not a key anybody has.
  • PlList and PlListItem. The list is the sheet and the rows are what is on it, so size, density, color and dividers reach a row through an InheritedWidget. A row's action is a separate focus stop from the row itself.
  • PlBreadcrumb and PlTimeline. Both take their members as descriptions rather than widgets — Flutter's own idiom, the one DataColumn uses. The reason is that both have to reason about their members: which step is the current page, how many there are, which the fold takes out, which connector is the last. A Widget is opaque and none of that can be asked of one.
  • PlDivider. A rule between two things, horizontal or vertical, with an optional label set into it. length wins over a tight parent — a divider very often sits in a Column with crossAxisAlignment: stretch, where a bare SizedBox would be handed a tight width and lose.
  • PlSkeleton. The shape of something that has not loaded yet, in the three shapes a layout is made of. The travelling highlight is one gradient slid across the box by a GradientTransform rather than a second widget laid out per placeholder, and it becomes a colour pulse where the platform has asked for less movement — kept running either way, because a skeleton that holds still is indistinguishable from an empty box that finished loading with nothing in it.
  • PlBlockquote. The rule, the quotation mark and the attribution. The mark is drawn rather than typed, unit for unit out of the same 16-unit box the React package's SVG uses.
  • PlHighlight. The search as well as the styling. It takes a String rather than a widget tree, which is the one real difference from the React build: a Widget is opaque, and there is no reaching the text inside one you were handed.
  • PlIcon. A glyph at a known size in a known colour, for whichever icon set the app chose. The glyph is told how big it is three ways at once — through IconTheme, through DefaultTextStyle, and by the box it is laid into — so an Icon, a CustomPaint that reads the ambient theme and a bare character all come out the same size. color is nullable and defaults to null, which is how "inherit" is spelled in a language with no such keyword.
  • PlAccordion and PlTabs. Both take their members as descriptions rather than widgets, and both fold the panel into the member that opens it — a PlAccordionItem carries its body, a PlTab carries its panel, so there is no second list to keep in step. The accordion's value is a Set<T> even with multiple off, because closed is a set too. The tab bar owns one focus stop, moves the choice with the arrow keys on whichever axis it runs, and builds only the panel that is open: a tab that is not open costs nothing, and the price is that its state has to be held above the bar.
  • PlTable. Laid out by Flutter's own Table, which is where the table, row, cell and column-header semantics come from — a screen reader announces the column with every number under it, for free. A column says how to get a cell out of a row and that is all it says: cell is required here, because Dart has no row[key] on an arbitrary type. Width comes in two forms rather than one — a length in pixels, or a flex share of what is left, which is what the React build's width: '30%' already meant. maxHeight caps the grid and the rows scroll inside the sheet past it, with the caption left above what scrolls. stickyHeader pins the column names over that scroll, and the thing that used to make it impossible is the thing it avoids: a pinned header is not a second grid here. There is still one Table with its header row in it, and the band laid over the top is a copy of that row with each cell put in a box of the width the grid actually laid the real one out at — so one grid still decides every column, and the band only repeats what it decided.
  • PlNumberField. The shell is a PlTextField's to the pixel, and what sits on it is a real numeric control: the arrow keys and the steppers move by step with Shift and Alt changing how far, a held stepper repeats after a pause, and the value clamps when it settles rather than while it is being typed. format is two functions rather than an Intl.NumberFormatOptions — one writes and one reads — because there is no Intl.NumberFormat in the Dart SDK and this package has no dependencies. The arrow keys are bound inside the field, closer to the editor than an app's own text-editing shortcuts, which is what keeps the up arrow moving the number rather than the caret.
  • PlOverlay and PlModal, and the layer under both. A sheet that takes the page away lifts itself into the nearest Overlay — where it is written does not matter, and it takes up no room there — and what it brings with it is the backdrop, the fade, the pointer held outside, focus held inside and put back on the way out, and Escape. Both are controlled: the × and a press outside report rather than act, which is why there is no trigger and no PlModalClose here. A modal's body is the only section that scrolls and the only one that gives way when the sheet runs out of screen — a header that scrolled away would take the modal's name with it.
  • PlTooltip, with the anchored layer under it. A popup that hangs off something rather than covering everything: a LayerLink keeps it stuck to its trigger while the page scrolls, and the collision handling is a flip and not a slide — sliding would need the position recomputed against the viewport every frame, which is what the link exists to avoid, and a wedge that has crept off its trigger points at nothing. open is a bool?, and null is the tooltip driving itself from the pointer, a long press and focus: the one place in the package where a component owns its own state, because nobody has an opinion about whether a pointer is resting on a button. PlTooltipProvider shares one delay across a toolbar.
  • PlSelect. The trigger is a PlTextField's shell wearing a chevron and the list is a PlTooltip's plate, both on purpose: a form where the select is a different height from the fields around it looks assembled rather than designed, and a screen with two kinds of floating sheet on it has one too many. It is generic in its value's type, which is the one place it parts company with the React build's advice — nothing is submitted here, so the value can be the thing itself. The keys stay on the trigger: arrows move the highlight, Enter takes it, Escape closes, and focus never has to go anywhere and come back. The trigger holds its width at the longest label it could ever say, laid out and not painted, so choosing a shorter option does not shrink the field out from under the pointer that chose it.
  • PlToast. A PlToastProvider wrapped once around the app, and PlToastProvider.of(context).show(...) from anywhere under it — a controller rather than a widget, because what a caller has at the moment a toast is warranted is a callback, not a place in the tree. The stack needs no Overlay: the provider is already above everything it has to cover, and nothing in the strip is told to ignore the pointer because an Align hit-tests its child and not the room around it. Showing an id already on screen changes that toast in place, showFuture makes one message follow a future from "working…" to its answer, and the clock stops while the pointer rests on a toast — which is a reader reading it.
  • PlChatBubble. The corner nearest the speaker cut short — the library's one piece of chat vocabulary, doing the job a drawn tail does elsewhere without hanging a triangle off a sheet of glass that was cut with a straight edge. The row runs the other way for the reader's own message by reversing its direction rather than its list, so a thread in Arabic is mirrored without being told twice. The typing dots light in colour only and never move; the delivery mark carries its own name for the readers a double tick says nothing to; and the link card's surface is mixed out of the bubble's own ink, because it is the one part that has to work on both a filled surface and a bare one.
  • PlFilePicker, which does not pick. This package has no dependencies and reaching the file system is a plugin's job in every Flutter app that does it, so onBrowse is where the app's own picker runs and everything after that is the component's: the rules, the list, the removal and the box. accept is applied to whatever comes back whether or not the plugin was told the same thing — a rule the component states and does not enforce is not a rule — and maxFiles counts against what is already held rather than against one batch, which is the difference between "you may add five files" and "you may end up with five". There is no OS-level drag either, so dragging is a flag an app that has one sets: the look of the state is the component's, the detection is the app's.

0.0.2 #

A packaging release. No component changed, and no code in lib/ did either — everything here is about what pub.dev was shown, and what it was shown was wrong in three ways.

Fixed #

  • The homepage pointed at a host that does not exist. The documentation is at plass.cdget.com; pubspec.yaml claimed plass-ui.cdget.com, which resolves to nothing. pub.dev checks the URL, so this cost the package its pubspec points outright.
  • The description was 195 characters, and pub.dev's ceiling is 180. Shortened to 174 without dropping anything it was actually saying.
  • The gallery was not in the published archive. .pubignore excluded example/, so the directory 0.0.1's notes describe as shipping was in the repository and nowhere else — and pub.dev reported the package as having no example. It ships now, along with an example/README.md that is a whole running app in forty lines, since the gallery's own main.dart opens on the machinery that lets the documentation site embed it rather than on anything a reader wants first.

0.0.1 #

The first release of the Flutter package, and a preview rather than a product. One component ships; what is actually being released is the shape everything after it will be poured into — the tokens, the scales, the theme and the test setup.

The design language itself is not new: it is the one the React package already ships, ported value for value. The documentation is one site for both, with a framework switch in the sidebar.

Added #

  • PlButton. variant (solid · glass · ghost), size, color, density and elevation off the shared vocabulary, plus startIcon, endIcon, loading, readOnly, disabled, fullWidth, semanticLabel and the usual focusNode / autofocus / onLongPress. onPressed: null disables the button, as it does everywhere else in Flutter.
  • PlassTheme, and the fact that you do not need it. A component resolves its tokens from the nearest one and falls back to MediaQuery.platformBrightness when there is none, so a button dropped into any app is already in the right theme and follows the system switch. The theme is an override, not a prerequisite.
  • PlassTokens — the whole token sheet as Dart: six colour families, three glass strengths, the elevation ladder, the tinted lift, the radii, the durations and the two curves. Four values are hand-picked per family and everything else is derived, so adding a family is one entry in PlassColor plus its colours.
  • The shared vocabularyPlassSize, PlassColor, PlassVariant, PlassDensity, PlassElevation, PlassOrientation, PlassSide, PlassAlign, PlassCorner. An md is 40px on everything, and it is the same 40px the React package means.
  • Nothing from material.dart or cupertino.dart. The package is built on package:flutter/widgets.dart alone, which is what lets it sit inside a Material app, a Cupertino app or a bare WidgetsApp without dragging a second design system in behind it — and what makes it indifferent to those two libraries moving out of the framework.
  • No dependencies at all, and no assets or platform channels.
  • A gallery, under example/. It runs as an app on any device, and its web build is what the documentation site embeds behind every Flutter preview — so the previews on the site are the real package, not a screenshot.

Matched to the React build on purpose #

Three things do not port across as the same number, and each is converted rather than copied:

  • Shadow blur. CSS defines a shadow's blur radius as twice the Gaussian standard deviation; Flutter's BoxShadow.blurRadius converts with σ = r × 0.57735 + 0.5. Handing Flutter the CSS number makes every shadow about a fifth softer than the one it is copying, so the ladder is converted on the way in.
  • The solid gradient. LinearGradient(topLeft → bottomRight) runs along the box's diagonal, which is only 135° on a square; on a wide button the sweep visibly flattens. The endpoints are computed per paint the way linear-gradient(135deg, …) specifies instead.
  • The glass hairline. BoxShadow only casts outward, and the light along a sheet's top edge is an inset shadow. It is reproduced exactly — the shape minus the same shape moved down a pixel, clipped back to the shape.

And two visible differences that are not bugs: the font is whatever the host uses (neither package sets one), and PlassDensity.standard is spelled 'default' in React, because default is a reserved word in Dart.

3
likes
160
points
417
downloads

Documentation

API reference

Publisher

verified publishercdget.com

Weekly Downloads

A Flutter UI component library made of glass and gradients — tinted surfaces, coloured shadows, and light that follows the pointer. Accessible, themeable, dark mode built in.

Homepage
Repository (GitHub)
View/report issues
Contributing

Topics

#ui #widget #design-system #glassmorphism #theming

License

MIT (license)

Dependencies

flutter

More

Packages that depend on plass_ui