plass_ui 1.5.0
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'squerytakes aList<String>rather than a list of patterns. ARegExpinside a list was escaped and searched for as the characters it is written with, soquery: [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 typing1 + 1is looking for1 + 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 Reactqueryhas always beenstring | 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.topis 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 isPlTextField,PlSelect,PlCombobox,PlNumberField,PlFilePicker, and the six that share a picker's trigger:PlDatePicker,PlDateRangePicker,PlDateTimePicker,PlTimePicker,PlColorPickerandPlTreeSelect.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 wantsfullWidth.PlOtpFieldis 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 ownlabelPlacementis 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 nearestBackdropGroupfor 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.copyWithandPlassColorFamily.copyWithare public, so an app can bring its own colours.PlassTheme.tokenstook a set, and the only sets that could be built werePlassTokens.light()andPlassTokens.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;withFamilyis 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,durationandeasestay 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.
PlassGlowLayerwas built for every interactive surface and reached four of them —PlButton,PlToggle,PlPillandPlChip— so a form of Plass fields answered a pointer with nothing while the button under it bloomed. It is now onPlTextField,PlNumberField,PlSelect,PlCombobox, aPlFilePicker's drop zone, each segment of aPlSegmentedButton, and the six triggers that share a picker's shell:PlDatePicker,PlDateRangePicker,PlDateTimePicker,PlTimePicker,PlColorPickerandPlTreeSelect.PlassTokens.fieldGlowandfieldFlashare the two colours for it, the family's own tint in all three variants rather than switching on the variant asglowandflashdo — asolidfield 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, aPlRadioand aPlSwitch'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. APlOtpField's slots are separate boxes with gaps between them, so there is no one surface for a light to cross. -
A
PlFilePickersays why it turned files away.onRejectedwas 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 amaxFilesof five is ninety-five lines of the same sentence, and what the limit is stays inhint.showRejections: falseturns it off for an app that says the same thing somewhere of its own, andonRejectedfires 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,filesRejectedSizeandfilesRejectedCountjoinPlassLabelsin all seven languages, which takes it to ninety-nine. -
A
PlFilePickerspreads a halo while a file is over it. The wash and the ring-coloured edgedraggingalready 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 aPlSlider'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
PlGalleryin 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 nearestScrollableabove 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.dasheddraws 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
categoriesleaves the positions out too, because the order of the reading already carries them.semanticValuestill 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
48300where the axis beside it said48.3K. Both now call onestackToFull, which is what the React build already did, and the label is written compactly unless aformatsays 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, asMath.floordoes 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
PlScatterChartmark's ring is as thin as the React build's. The surface showing through around each mark was stroked at twicemarkGap, 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
formatof its own an axis, a tooltip and the summary wrote48300where the web wrote48.3K, and1.50where the web wrote1.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 nointland a separator is the locale's to choose; pass aformatfor one. -
A time axis whose two ends are the same moment opens a day around it.
minandmaxnaming 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
crosschart 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
PlAnimateMarqueewith aspeedof 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, aspauseddoes, and an explicitdurationstill decides on its own. -
A minimized
PlWindowPanekeeps 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'sinertbody already did. -
A
PlStepperpanel 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'saria-labelledbydoes. A step whose label is not text is left as it was. -
A
PlFloatingBottomNavigationwith noonChangedstill 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 rulePlBottomNavigationalready follows. -
An unavailable destination in a
PlBottomNavigationis dimmed. APlBottomNavigationItemwithdisabledwas 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 theopacity-50 saturate-[0.35]of the React item and whatPlFloatingBottomNavigationalready did with its own. -
A
PlHotKeysshortcut 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 aPlButtonor aPlCommandPaletterow, ⌘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
PlColorPickertrack 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
semanticLabelon aPlImageis no description at all.semanticLabel: ''left the picture on the semantics tree with an empty name, and named apreviewbutton "— preview". Empty now says whatalt=""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
PlChatBubblerun 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.removeLabelandPlCombobox.removeLabelare 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.defaultValueLabelstill returns the English sentence. -
Without
rowKey, aPlDataTablerow is keyed by its position inrows, and every callback'sindexis that position. The default key and theindexhanded torowKey,cell,onRowPressedandisRowSelectableused to be the row's place in what was drawn, whileonSelectedChangedlooked the rows up by their place inrows. Ticking the first row on page two also showed the first row on page one as ticked and handed backrows[0], and a sort or a search did the same. Both now count from the top ofrows, so a row keeps its key and itsindexwherever a sort, a search or a page puts it. Code that usedindexas 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, androwKeyis still the way to key rows that surviverowschanging. -
A
Spaceror anExpandedin aColumndirectly inside aPlGridcell throws. A row now lays each cell out at its own height before stretching it, so on that first pass theColumnhas 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 aLayoutBuilderthrow. To pin a footer to the bottom of a stretched cell, give theColumnMainAxisAlignment.spaceBetweeninstead. -
Picking a day in
PlDateTimePickerkeeps the moment insideminDateandmaxDate. 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. WithminDate: 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, withshowSeconds) at or afterminDateand down to the last one at or beforemaxDate. A clock that is already inside the bounds is kept as before.
Added #
-
PlTimelineItemtakes akey. Steps were matched by position, so inserting a step at the start of aPlTimelinehanded each statefulchildthe state of the step that had been in its place, and a key on thechildcould not help. A step is now matched by its item'skey, which is what a React element'skeydoes. Without one, steps are matched by position as before. -
PlCardtakes aheadingLevel. The title's depth in the screen's outline,1to6, which is whattitle={<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 onPlConfirmProvider's alert),optional(forPlStep.optional, which takes a widget),sidebarOpen(PlSidebarTrigger),spoilerWarning(PlSpoiler),filePickerTitle(PlFilePicker), and the five words aPlChatBubble's mark says,messageSending,messageSent,messageDelivered,messageReadandmessageFailed. All seven packs translate them.PlConfirmProvider.acknowledgeLabelis now nullable and falls back to the pack. -
labelonPlModalandPlDrawer, the name a screen reader announces the layer with when it opens.titleis a widget and has no text to hand over, so both layers used to open with no name at all. It is the same parameterPlOverlayandPlCommandPalettealready have. -
PlAspectFit.scaleDown, forPlAspectRatioandPlImage. It iscontainthat never enlarges something smaller than the box. -
PlImagetakes awidthand aheight. 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 loneheightis a box that tall across the width it is given, taking its width from aratiowhen there is one, and a lonewidthis a box that wide, never wider than the space it has. A narrowed box sits at the start of its space, andpreview's press target and focus ring stay on the box. -
PlImagetakes aposition. AnAlignment, which decides which part of the picture acovercrop keeps and where the other fits leave their empty space. It is read on the picture as it is shown, soAlignment.topCenterkeeps the top of what the reader sees throughrotateandflip, with the same arithmetic as the React build. It is typedAlignmentrather thanAlignmentGeometry, so it stays on the same side of the photograph under a right-to-left direction. -
PlImagetakes aletterbox. It fills the part of the box thatcontain,noneandscaleDownleave empty.PlImageLetterbox.blurdraws the picture itself behind it, covering the box and blurred, turned, mirrored, placed and treated like the picture, from the sameImageProviderand so from the same cache entry. It is off the semantics tree and takes no pointer.PlImageLetterbox(decoration)paints anyDecorationbehind the picture instead. -
PlImagePlaceholder. Given as aPlImage'splaceholder, it stands a small copy of the picture in while the file arrives, from anyImageProvider. It is drawn under the picture with the picture'sfit,position,rotate,flipand treatment, stays until the picture has finished fading in over it, and is then removed in one step.blurblurs 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. -
PlImagetakes arotate.0,90,180or270degrees clockwise, and any other number goes to the nearest quarter, rounded the same way as in the React build. The picture is turned with aRotatedBox, so a picture on its side is laid out on its side: aratiois 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, andpreviewopens the picture turned the same way. -
PlImagetakes aflip.PlImageFlip.horizontal,verticalorboth, along the axes the picture is shown on, so a horizontal mirror swaps left and right on the screen whether or notrotatehas turned it.previewopens the picture mirrored the same way. -
PlGallerycarries the newPlImageparameters to its pictures. APlGalleryItemtakesrotate,flip,positionand aPlImagePlaceholder, and the gallery takesfit(stillPlAspectFit.coverby default) andletterboxfor every tile. An item'sratiostays 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. -
PlWindowPanetakes aresizable. 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.minWidthandminHeightare the floor it stops at,onResizereports the size, and a drag on a leading edge reports throughonOffsetChangedas well.resizeLabelnames 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.resizeWindowhad been inPlassLabelsand in all six locale bundles since the widget shipped, waiting for this. -
PlImagetakes afilter. Six named treatments —grayscale,sepia,saturate,desaturate,contrastanddim— resolved to the same amounts the React build writes into its CSS chain, so asepiais one colour across the two packages.colorFilteris the escape hatch and takes anyColorFilterof your own, in place of a CSS chain that would mean nothing here.internal/css.dartgainssepiaFilterandcontrastFilteralongside the brightness and saturation matrices it already had. The treatment goes on the picture and not on the skeleton or the fallback. -
PlImagetakes awatermark.PlImageWatermarksays what the mark reads, where it goes, how visible it is and at what angle;PlImageWatermarkPlacement.tilecovers the whole picture instead of a corner. A tiled mark is oneCustomPainterfor 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 intopreview. -
PlTabsanswers 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 aPlScrollZone, andwheel: falseturns it off.internal/wheel.dartis the shared half of the two. -
PlScrollZoneandPlTabstake anoverscroll.PlassOverscroll.contain, the default, orPlassOverscroll.auto. See Changed for what the default alters.
Fixed #
-
A
PlTabsbar with more tabs than room opens with the chosen tab in view. A bar whosevaluewas 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
PlBottomNavigationwith noonChangedstill 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
PlCommandPaletterow 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
PlAccordionsection keeps its body until the panel has closed. The body vanished the momentvaluestopped 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
PlAccordionheader is a heading. A screen reader's heading navigation passed over the sections, since nothing marked a header as one. Each header row, with itsaction, 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
PlCalendaris 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. Whiledisabledis set or there is noonChanged, each is now announced as disabled, with no action. The calendar looks the same. -
A labelled
PlImagepreview says that it opens a preview. Its press target was named bysemanticLabelalone, so a screen reader heard what the picture is but not what pressing it does. It is now named by the label and thenpreviewLabelin lower case, or the label pack'spreviewwhen that is left out, "A portrait — preview", as the React build names its button afteralt. -
The typing dots of a
PlChatBubblekeep moving when animations are disabled. WithdisableAnimationson, 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
PlColorPickerthumb 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
readOnlyPlRadioGroupis 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'saria-readonlyon the group says the same. -
A
readOnlyPlSwitchis 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'saria-readonlysays. -
A
PlColorPickerthumb sits on its value at everysize. Each thumb was placed as if it were themdsize, so atxs,sm,lgandxlits centre was up to 2 pixels beside the colour it showed, on the square and on the rails. -
A
PlColorPickerswatch 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,PlNumberFieldorPlOtpFieldleaves 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 disabledPlButtonis and as in the React build. A read-only field still takes the focus. -
A
readOnlyPlCheckboxis 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'saria-readonlysays. -
A
PlCalendarwith noonChangedis disabled. Its dartdoc said a calendar nothing listens to is inert, but onlydisabledtook 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, asdisabledmakes it and as every other control without a callback is. -
The two select-all ticks of a
PlTransferhave 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 byselectAllLabeland then by its list's heading, "Select all Available". -
End on a full
PlRatingkeeps the full score. Withclearableon, 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 calledonChangedwith the 0 it already had. Home and End now go to the end of the row, and callonChangedonly when the score moves. -
A pressable
PlChipdraws 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
PlImagepreview with nosemanticLabelhas a name. Its press target was named only bysemanticLabel, so without one a screen reader found a button with no name. It is now named bypreviewLabel, or by the label pack'spreviewwhen that is left out, as the React build names its button whenaltis 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 coversPlDatePicker,PlDateRangePicker,PlDateTimePicker,PlTimePicker,PlColorPickerandPlTreeSelect. -
A
PlTreelets go of the focus node of a row removed fromitems. 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
PlTreebranch with nothing to step into keeps the focus where it is. A branch withchildren: [], or whose children are alldisabled, 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
PlMockupsystem 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
PlImagetells 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 carriessemanticLabel. An image with nosemanticLabeland nopreviewis now left off the tree entirely: its image flag merged into the widget around it, so aPlButtonholding one was announced as an image. The default fallback no longer repeats the label. -
The segment of an indeterminate
PlProgressLinearenters 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
disabledon while aPlTooltipis open no longer makes the rest of itsPlTooltipProvidergroup 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 switchingdisabledon calls off the wait. -
A
PlStatchange that rounds to 0 is drawn as no movement.change: -0.04was written-0%and still drawn with a ▼ in the danger colour. It is now0%, muted and with no arrow, aschange: 0already was. -
The
PlConfirmProviderdialog 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. -
PlTourmoves to its target at once when the reader has asked for less motion. It always scrolled each target into view over 260ms, so withdisableAnimationson the screen still slid past on every step. It now jumps there and measures the light once the jump is laid out, asPlBackTopalready jumps. -
The lowest bar of a
PlSparklinebar 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
PlCodeBlockhighlightLinesrange stops at the last line, and a number too long for anintno 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'madeint.parsethrow while the block was building. Only the lines the block has are walked now, and a number too long for anintcounts as past the last line, as in the React build.parseLineSpectakes the bounds as the optionalfirstandlast. -
A
PlSparklinewithshape: PlSparklineShape.barwhose 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
PlTimelineChartspan that lies wholly outside a fixedminandmaxcan 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.lastlabels 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, andextremesno longer walks the whole series again for every bar on each paint. -
The × on a
PlChip, on aPlComboboxchip 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 coversPlDatePicker,PlDateRangePicker,PlDateTimePicker,PlTimePicker,PlColorPickerandPlTreeSelect. -
A
PlHeatmapChartthins 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
PlScatterChartreads a bubble'sz, and stops reading a series switched off in the legend. The text a screen reader is handed wrote each point asx, yalone, although the docs say thezfollows in brackets. It also read every series that had not startedhidden, so one switched off in the legend was still read out. It now writes1, 1 (100)as the readout does, and reads only the series that are on. -
A tiled
PlImagewatermark 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
PlGallerytile is described by the caption it draws. The tile's semantics replaced everything under it with the picture'ssemanticLabeland its place in the set, so thetitleanddescriptionon it were never read. The lines a tile draws are now its semanticshint, aPlGalleryCaption.hovercaption included before the pointer has shown it. The label is unchanged. -
A
PlCodeBlocktells 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 announcescopiedLabelorcopyFailedLabelonce the clipboard has answered, as the React build does witharia-pressedand its live region. -
A
PlCodeBlockis named after its language beforecodeLabel, as in the React build. Withlanguage: 'dart'andcodeLabel: 'Code', a screen reader heard the code called "Code" rather than "dart". The language now comes first, thencodeLabel, then the label pack's word for code, and a blanklanguagecounts as none. -
A chart legend asked for on the left or the right stays on that side under RTL.
PlassSideis a physical direction, but the legend was placed by the order of a flex row, which follows the writing direction, soside: '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
zlooked 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.itemshows the one series the pointer is nearest, andnonesilences aPlPieCharttoo.itemdrew the whole column, which is the tooltipcolumnalready gives, and a pie drew its readout whatever the mode said, since onlyhiddenstopped 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 Reactitemdoes, and a pie draws no readout undernone. -
PlassLabelsandPlDateNamescompare by their words. Neither had an==, so a pack written where the documentation recommends writing it,labels: ko.copyWith(start: '체크인')insidebuild, was a new object on every frame,PlassTheme'supdateShouldNotifyanswered 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 itsdelay. 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-seconddelayskipped the other 900ms. What is left of the wait is now what is waited out, as the React build does. -
A
PlTransferrow that leavesitemsand 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 leftitemsare now dropped as they go. -
Removing a
PlAnimateTypingwith a caret under reduced motion no longer throws. WithMediaQuery.disableAnimationson, the caret never built its blink controller, sodisposebuilt 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 defaultcaret: true. The controller is now built with the caret, and it is not started while the caret stays still. -
PlAnimateSplitandPlAnimateScramblekeep 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. -
PlAnimateSplitwithPlAnimateSplitBy.characterwraps a line between words. Every character was a child of oneWrap, so a narrow line broke partway through a word. The characters of each word are now laid out in aWrapof 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.visiblewaits until the widget is on screen, not only inside the nearest scrollable. It watched the nearestScrollablealone, so aPlAnimateCounterin 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'sIntersectionObservermeasures. A widget moved under other scrollables follows the new ones. -
PlAnimateHeadlinekeeps 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,delayor the running state changes. -
A hover effect on content that takes no focus adds no stop to the tab order.
PlassAnimateTrigger.hoverwrapped the child in aFocusableActionDetector, which takes the focus by default, so Tab stopped on everyPlAnimate*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 aMouseRegionand 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
PlAnimateMarqueeitems 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
PlAnimateMarqueeafter the first. The copies were behindExcludeSemantics, 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 behindExcludeFocusas well, and only the first copy's children take the focus. -
PlRatingwrites a fractional score the way the pack's language writes a number. The packs interpolated the rawnum, so a half star read2.5 von 5in German, French and Spanish, and an average such as7 / 3read 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 anintldependency, and a whole score reads3even when it arrives as3.0. -
A
PlChipdelete affordance is named in the word order of the label pack. The name was the pack'sremovefollowed by the chip'sText, English's order in every language, so a Korean screen read삭제 Design. It now comes from the pack'sremoveItem,Design 삭제in Korean. A chip whose child is not aTextis still named byremovealone, anddeleteLabelstill replaces the whole name. -
PlGallerysays 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 readA harbour — 2 of 4. They now come from the newPlassLabels.galleryItemfield, translated in all seven packs in each language's own word order, anditemLabelstill wins. -
The
PlWindowPaneminimize, 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
PlAlertis announced when it appears, whatever its severity. Only awarningordangeralert was a live region, so aninfoorsuccessalert 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 adangeralert is read when the reader pauses rather than interrupting. -
Each
PlCarouseldot 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. -
PlCarouselautoPlaykeeps 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 whenautoPlay,interval,value, the number of slides or whether there is anonChangedchanges. -
Pressing a
PlTransferarrow 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 newtransferMovedlabel field. The newmovedLabelparameter 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 ofPlFilePickerandPlCombobox, andPlHowToSteps' step positions were English templates inside each widget. They now come from newPlassLabelsfields (paginationPage,ratingValue,ratingNone,carouselSlide,removeItem,addCustom,howToStep), translated in all seven packs in each language's own word order, and the packs stayconst. A widget's own parameter still wins. -
A
PlSpoilerno 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'sState, 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 itsStateacross a reveal and back. -
Pressing Reveal or Hide on a
PlSpoilerfrom the keyboard no longer sends the focus out of the spoiler. The pressed button was put underExcludeFocusby 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
PlSliderrun 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
fullWidthpicker no longer lays out its width samples.PlDatePicker,PlDateRangePicker,PlDateTimePicker,PlTimePickerandPlColorPickerbuilt every sample under the value, invisible, even whenfullWidthmade the field's width its container's. AfullWidthfield now builds only what it shows, asPlSelectalready does. A field withoutfullWidthkeeps them. -
A press on an open
PlPill'sdetailsor on itsendIconno longer callsonPressed. 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
PlPillbutton says whether itsdetailsare open. Withdetailsand anonPressed, the button reported no expanded state to a screen reader. It now reports whether the panel is expanded, and a pill withoutdetailsreports no expanded state at all. -
A
PlCardwithinteractiveand noonPressedlifts under the pointer. Only a pressable card tracked the hover, sointeractiveon its own drew the card at rest whatever the pointer did, although the documentation said it gave the same lift asonPressed. 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
PlCardno longer builds its content again when the pointer moves on or off it. The lift was aTransform.translateput around the sheet only while it was raised, and the focus ring aCustomPaintput 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
PlCollapsiblewithkeepMountedkeeps 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 whatkeepMountedis 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
semanticLabelwas 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
PlToastis announced, including one with the defaultlowpriority. Only ahightoast 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 ahightoast is read when the reader pauses rather than interrupting; thealertandstatusroles that would carry the difference cannot be set on a live region. -
A
PlNavigationMenuItemdestination can mark the screen the reader is on. Items had noselected, so the row could not say which destination was current.selectednow marks a destination selected for a screen reader and draws its word in the accent. -
A
PlMenuSubmenuopens 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-leftDirectionalitythe arrow key that opens it is the left one, and the chevron kept pointing right. It now opens to the left under RTL, asPlNavigationMenualready did, and the chevron points that way. -
Pressing a
PlAnchorrow stopsoffsetabove 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 thatoffsetwas 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 whenMediaQuery.disableAnimationsis set. -
PlScrollAreaandPlScrollZonecan be scrolled from the keyboard. Neither took the focus, so a box of text with nothing focusable in it, or a strip withbuttons: 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
PlTransferarrows 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-leftDirectionality, but the arrow to it was always drawn pointing right and the arrow back pointing left. Both now turn over with the direction. -
A
fullWidthPlSelectno 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 whenfullWidthmade its width the container's, so a label with a picture in it loaded that picture for every option. AfullWidthtrigger now builds only what it shows. A trigger withoutfullWidthstill lays out every label, because that is what keeps it from changing width with its value. -
A
PlSelector picker trigger is named by the field'slabel. Without asemanticLabel, the trigger had no name of its own: aPlSelecttook the chosen option's words as its name as well as its value, so it read "Seoul, Seoul" with "City" nowhere, andPlDatePicker,PlDateRangePicker,PlDateTimePicker,PlTimePicker,PlTreeSelectandPlColorPickerread only the value. Whenlabelis aText, 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 needssemanticLabel. -
The
PlSegmentedButtontile 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
PlSliderthumb 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,PlNumberFieldandPlComboboxtake 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 aPlCombobox.multiplelost the connection again when its first chip appeared. The field now keeps the same editor through both. -
The
PlAnimateTypingcaret 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. -
PlPaginationmarks 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
PlNumberFieldstepper settles once, when it is let go, and stops at the end of the range. Every repeat of a held stepper calledonCommitted, every 60 milliseconds, and the repeats went on after the value had reachedminormax, so a caller that saved ononCommittedsent one request after another. A repeat now callsonChangedalone, the repeats stop at the end of the range, and letting go callsonCommittedonce with the value it reached, without adding a step of its own. -
A
PlNumberFieldshows 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 invalue: 5, onChanged: (_) {}, left 40 in the box while the value stayed 5. Once the parent has rebuilt, a box that disagrees withvaluenow goes back to it. -
A
PlNumberFieldis named by itslabel. 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 onPlTextField, and the steppers sit directly under it. -
A
PlNumberFieldwithallowWheelScrubkeeps 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
extendedPlFloatingActionButtonsays 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
PlFormchecks every change after a submit, asonSubmitsays. The defaultvalidationModenever 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
PlComboboxtakes the lit row and keeps the field focused. The editor gave the focus up on Enter, which closed the list, so amultiplecombobox 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
PlColorPickerswatch 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
inlinePlColorPickeris grouped under itslabel. 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
PlColorPickeranswer ↑, ↓, 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
PlFilePickerbutton 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 withlabeland 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
PlCalendarand 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, aPlMenu, aPlSelectorPlComboboxlist, a picker, and aPlNavigationMenupanel 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 aPlMenubarword 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
PlConfirmProviderqueue places the focus again. The sheet stays open between two queued questions and reused its buttons, soautofocusdid 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 owninitialFocusdecides 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
showFuturestays 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. -
PlToursays 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'stitleis now a live region, or itscontenton 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
PlTourtakes 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 settingopentofalsealso 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
PlTableor aPlDataTableno 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 hundredhoverablerows dropped frames under a moving mouse. The bands are now painted behind the grid, and a move repaints them without building a cell. -
A
PlTypographyheading carries its level.h1toh6were 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,1forh1through6forh6. -
Each
PlChipdelete 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. Whenchildis aText, 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 adeleteLabelis still the whole name. -
The picture open in the
PlGalleryviewer has a name. The large picture was drawn with nosemanticLabel, so a screen reader announced an unnamed image. It now carries the item'ssemanticLabel, as its tile does. -
A
PlDataTablesort keeps rows that compare the same in the order they came in. Dart'sList.sortdoes 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
PlDataTablewhose parent holds the sort stays unsorted when the parent clears it. Asortofnullwas 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
PlDataTablethat keeps its own page callsonPageChanged. 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 unlesspagewas passed as well. -
A
PlDataTablesorted 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 owncompareis still turned round as a whole. -
A
PlHighlightmark 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
PlCodeBlockwith 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
PlTimelineChartwhose 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 days09:00could be either day, and the tooltip and the summary a screen reader reads wrote09:00 – 17:00with 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
PlScatterChartwhosexvalues are dates ticks its x axis like a calendar, andxAxis.formatwrites its ticks. The axis ran on the dates' milliseconds with the steps a count gets and wrote each tick as a thirteen-digit number, andxAxis.formatwas not applied to it at all. It now takes the steps and the labels of a time axis, asPlTimelineChartdoes, andxAxis.formatwrites the ticks of any value-scaled x axis. -
An area is filled between its two edges.
PlAreaChartand aPlSparklinewithshape: PlSparklineShape.areabuilt 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 asmoothorstepcurve, 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
fulltells a reader the caller's numbers. The tooltip and the summary ofPlBarChartandPlAreaChartwrote the share each part was drawn at through the caller'sformat, so a currency format readNew $20for a part worth 4,000. They now read a point'slabel, as the React build does, and a chart stacked to full sets that label to the caller's number in the caller'sformat. -
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 withmin: 0was ticked every 0.5 where the React build ticks it every 0.2. Only the free end opens now, in both builds. -
emptyon a line, bar, area, scatter or timeline chart is drawn as given. Those charts read the words out of a plainTextand ignored any other widget, so aText.richthrew 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, asPlPieChartandPlHeatmapChartalready drew theirs, and those two now give a plainTextthat type as well. -
A chart legend beside the plot is a list. With
PlChartLegend(side: PlassSide.left)orright, the legend ofPlLineChart,PlBarChart,PlAreaChart,PlScatterChartandPlPieChartwas 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,PlTimelineChartandPlHeatmapChartis 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.datereached the axis, the tooltip, the pie's legend and the heatmap's columns and summary throughtoString, so a tooltip was headed2026-03-01T00:00:00.000. They now write the short month and the day,Mar 1, in the names of the theme'sPlDateNames, as the React build does. -
A chart no longer rebuilds for every pixel the pointer moves.
PlLineChart,PlBarChart,PlAreaChart,PlScatterChartandPlTimelineChartstored 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.cssto keep the two in step. -
PlConfirmProvider,PlPopconfirmandPlCommandPalettesay 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,cancelLabelandplaceholderare now nullable and fall back to the pack'sconfirm,cancelandcommandPalettePlaceholder. -
The row the arrow keys move to stays in view in
PlSelect,PlComboboxandPlCommandPalette. 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,PlDrawerandPlToastProviderkeep 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,PlTooltipandPlHoverCardignored it, and inside aPlModalit closed the modal instead of the popover opened in it, withoutPlPopconfirm'sonCancel. Escape now closes the nearest open layer first, and cancels a popconfirm.PlTooltipandPlHoverCardalso 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,PlRadioGroupandPlSegmentedButton, and only those two turn round inPlRating. 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.PlRatingturned 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
PlWindowPanewhose offset is fed back moves with the pointer. The window added the distance it had been dragged tooffset, and reported the same sum, so a caller passingonOffsetChanged's value back intooffset, as the documentation's own demo does, moved it twice as far as the pointer went. GivenonOffsetChanged, the window is now drawn atoffsetalone 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 toonOffsetChangedhas to pass the value back for the window to move. -
A
PlGridcell can hold anything that measures itself with aLayoutBuilder. Every row was wrapped in anIntrinsicHeight, which asks each cell for an intrinsic height that aLayoutBuildercannot give, so aPlGridinside a cell threw, and so did a chart, aPlTable, aPlSlider, aPlBadgeor 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
PlSidebarkeeps 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 anonResizethat calledsetStatestopped the drag from moving at all. -
A
PlSlidercan 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 onestep, and a value is read in the decimals itsstephas 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
PlTimePickerandPlDateTimePickerwork 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
PlToastthat 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 itsonClosewas called a second time and its disposed fade threw. A closing toast is now off the clock and cannot be dismissed twice. -
A sortable
PlDataTableheading 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
PlHeatmapCharttreemap 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
hiddencan be switched back on from the legend. The chart kept readinghiddenas well as the legend's own switches, so pressing the entry changed nothing.hiddennow 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 aPlScatterChartwere 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,PlOverlayorPlConfirmProvidersheet 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
PlTreerow, aPlMenurow, aPlNavigationMenuword and panel link, a reachablePlStepperstep, aPlAnchorrow, aPlImagewithpreview, aPlOtpFieldand 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,PlRadioGroupandPlSegmentedButton. 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
PlImagefades 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. -
PlSpoilerno 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 withVisibility(maintainSize: true)andExcludeFocus, exactly as thereversiblehide row already was, so the sheet measures the same in both states and the hidden cover is off the semantics tree. AmaxHeightclamp is still released on reveal, which is the one thing that may resize it.
Changed #
-
A
PlSkeletonruns 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,PlImageand thePlGalleryviewer 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 plainImageProvideris 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.PlImagemeasures its box on the first frame and asks for the picture on the next, and decodes again only when the box grows. AResizeImageyou pass is used as it is, andfit: PlAspectFit.nonestill decodes the whole file. One consequence:precacheImagewith a plain provider warms a different cache entry from the one these widgets draw, so precache aResizeImageand pass the same one. -
PlComboboxandPlCommandPalettesay the label pack'semptywhen nothing matched. TheiremptyMessagedefaults, "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 toempty, asPlTreeSelectandPlTransferdo, which makes the English default "Nothing here" for both. PassemptyMessageto keep the old words. -
A chart series with no
nameis called by its number. The legend, the tooltip and the summary ofPlLineChart,PlBarChart,PlAreaChartandPlScatterChartcalled it "Series 2", a word no pack translates, whilePlHeatmapChart,PlTimelineChartand the React build say "2". They now say the number as well. -
A long
PlComboboxorPlCommandPalettelist 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.ringwassolidat 55% opacity, which came to about 2.2:1 against a white surface, and 1.4:1 forwarning, under the 3:1 a focus indicator needs. It now returnsaccent, 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
PlScrollZoneno 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 newoverscrolldefault isPlassOverscroll.contain, andPlassOverscroll.autois the old behaviour. Evenautonow 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
PlButtondoes, includingreadOnly, butPlFloatingActionButtonhas noreadOnly,density,onLongPress,focusNodeorautofocus, and the Flutter props table left out thecolor,loadinganddisabledit does have. The page, the table and the dartdoc now match the widget, and thecornerandfloatingexamples 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.headingLevelexists, andPlTypographyalready uses it. The table and the Accessibility section now point toPlCard.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 aftercodeLabel, 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,PlChartLegendandPlChartTooltip, 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.mdcarries 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
minimizedrolls the window up to its bar because a page has nowhere to send it.internal/window.dartholds the reference data — eight systems' worth of title bars, three buttons each, drawn five different ways — and every glyph is aCustomPainter.Three differences from the React build.
controlsis aSetwhere React takesboolean | array, since the order is the system's anyway.activeis 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 noresizable— the eight handles are a pointer affordance that a Flutter caller would more naturally build with their own gesture detector around the window.explicitChildNodesis 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 whataria-labelledbydoes in the React build. -
Four words in
PlassLabels—minimize,maximize,restoreandresizeWindow— 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.dartholds the reference data — five resolutions per device, three shells, six systems' worth of chrome — and every glyph in that chrome is aCustomPainterrather than an asset.Two differences from the React build, both because there is no CSS to lean on.
wallpapertakes aDecorationwhere React takes a background string, and there is noscroll: 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.dartgainsPlChartTimeUnit,timeScale,TimeScale,formatTimeValueandformatTimeTicks, matching the React half function for function: the steps a clock and a calendar actually have, walked withDateTimeso a month is a month and a daily axis stays on midnight across a daylight-saving change. The month names come offPlDateNamesrather than a platform formatter, which is the same trade the date pickers make — this package takes no dependency onpackage:intl.The frame grew three things for it: a
scalea chart can hand over already worked out, amarkHeadingfor marks that name themselves rather than taking their series' name, andrx/ryonPlassChartMarkso 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. -
PlassTimelinePointandPlassTimelineSeriesintypes.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.dartgainsPlChartScaleKind,rampStep,squarifyandTreemapTile, matching the React half function for function.PlassTokensgainschartSequentialOnandchartDivergingOn: 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.PlassChartScaleLegendjoinsinternal/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:
valueLabelsisPlHeatmapLabelsandscaleis the re-exportedPlChartScaleKind. -
PlGaugeChart. One number on a scale that is known in advance, drawn as a dial.A
PlMeterbent into an arc, sharing its vocabulary exactly, and not aPlPieChartwithPlPieShape.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
Stackover the painting rather than something painted, so it takes the caller's owncenterandcaptionand stays in the semantics tree. Its size is solved against the chord the hole leaves rather than fixed. The arc animates with aTweenAnimationBuilderon the swept fraction, at the samefillDurationa meter's bar uses, and honoursMediaQuery.disableAnimations.ticksisint?where React takesnumber | false— Dart has no union to put a count and a switch into one prop, sonullis off.ringPathjoinsinternal/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, intypes.dart, with the band rule ininternal/threshold.dart.PlMeterandPlGaugeChartread the same list the same way.PlMeterThresholdstays 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
minandmax.The one thing it does not share with the React build is how the colour is named:
colortakes aPlassColorfamily andtinttakes an exactColor, where React has a single prop typedPlassColor | (string & {}). Dart has no union to flatten, and one prop that accepted both would have to guess which was meant.Without a
semanticLabelthe whole strip is wrapped inExcludeSemantics, which is the Flutter half of what React does by puttingaria-hiddenon 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
zis a bubble rather than a dot. Azis 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.valueturns the category axis into a second number line — its ticks become the axis' categories, so every measurement downstream stays one code path.PlassChartMarkand a builder replace the column hit test with a nearest-mark search,markInsetreserves room on all four sides for marks drawn from their centre,swatchlets a chart put its own shape in the legend, andmarkReadoutandsemanticValuelet it say what a mark is worth.chart.dartgainscategoryNumber,pointX,categoryExtentandbubbleRadius, matching the React half function for function.shapeisPlScatterShape, a seven-member enum, where React takesMarkShape | '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, andPlassChartTooltipCard, so a readout is the same glass wherever it appears.arcPathjoins them ininternal/chart.dart, taking its angles in degrees clockwise from twelve — the frame a caller writes astartAnglein.valueLabelsisPlPieLabels, a two-member enum, rather than the four-memberPlassChartValueLabelsthe cartesian charts take:lastandextremeshave 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.
horizontalswaps 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.PlassTokensgainschartGrid,chartAxisandchartBaseline, so the two builds draw a chart's furniture from the same three greys.stackingis three named states rather than React'sboolean | '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 asmooththat curves differently depending on which widget a caller reached for.areaPathgrew a second edge with it, because a stacked band's floor is the band below it and moves with every category.stackingis one enum with three states rather than React'sboolean | '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.smoothis 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.chartSequentialandchartDivergingare the two five-step ramps, for a mark whose colour is a magnitude rather than an identity.internal/chart.dartis 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.dartis 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'slegend={false}— Dart has no union type — and itssideandalignare the library's ownPlassSideandPlassAlign.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.gridis a contact sheet,masonrykeeps each picture's proportion and stacks the columns,justifiedkeeps the proportions and fills every row to the edge, andquiltedis 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-growand a quilt withgrid-auto-flow: dense; there is no such thing here, so both pack themselves inside aLayoutBuilder—justifyRowsandquiltCells, 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.previewopens 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 aPlCarousel: no autoplay, no wrap, and the arrows stop at the ends.A tile is a
Semanticsbutton 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,autoandmono, which has no hue in it at all — and eight ports kept at the hex they were published in. APlCodeThemeis 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 iscustomThemeand 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 aPlCodeTokenKindon 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.highlightLinesis aStringhere and a number, string or list on React:'4','4-9'and'1,4-9,12'cover every case, and a DartObject?that had to be type-tested at runtime is not an API.Five new labels —
copy,copied,copyFailed,rawandcode— in all seven packs. -
PlTreeSelect. A value chosen out of a hierarchy rather than out of a list — aPlTreebehind 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.
selectableBranchesis 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 ownselectableoverrides 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.searchablekeeps 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 noString.normalize, and the React build's accent folding is the one thing the two searches do differently.A node's
labelis aStringand not a widget, which is the divergencePlTransferItemalready 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 nosearchLabelon this side — the label is already the words.valueis a controlledSet<String>, as every input in this package is.expandedandopenare the two that may be left out, and then the picker keeps them itself.No new label:
search,emptyandclearwere 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
PlHowToStepsturned 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 aBackdropFilter: 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, keepsEscape, and takes neither the route nor the focus.targetis aGlobalKeyand 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,controlleris the same parameterPlAnchortakes 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.PlTableis 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 intointernal/table.dartasPlassGrid, 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
columnsandrowsand a server-backed one is the same code withmanualand four callbacks. The starting values areinitialSort,initialSearch,initialSelectedandinitialPage, which is this framework's own convention rather than React'sdefault….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.dartalready gives: Dart's core has noString.normalizeand this package has no dependencies. The React build useslocaleCompareand the page says which is which.valueis required on any column that sorts or searches, not only on a drawn one:cellreturns 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-sortis 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.sortedAscendingandsortedDescendingare new inPlassLabelsand have no React counterpart.selectRowis 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.flexgained anullfor 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,koandzhHans, in a library of their own rather than behindplass_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.
closeis 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:paginationNextmoves by a page andcarouselNextmoves by a slide.PlassLabelsis whatPlPickerLabelsgrew into, andPlPickerLabelsis 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;copyWithis 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, answeringPlassLabels.englishwhen 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. APlAnimateAppeartells 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
PlAnimateAppearspells 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.characterbreaks 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 aWrapbreaking 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
tickrather than at the refresh rate. The settling line is wrapped inExcludeSemanticsand 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 whosetriggerdefaults toPlassAnimateTrigger.visiblerather thanmount: a count that ran off screen delivered a number that was already sitting there.formatValueis a callback rather than an options object, forPlProgressLinear's reason. The ticking figure is wrapped inExcludeSemanticsand 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, sotriggerdefaults toPlassAnimateTrigger.manualand it starts held still.replayis what it exists around: a refusal can happen twice and aboolcannot 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.PlassAnimateSettingstakes anoncefor it, and the gate callsrestartrather 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 thePlAnimate*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.
curvedefaults toCurves.easeInOutrather 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. -
PlHowToStepsandPlHowToStep. Instructions, numbered, with what to do under each one — the Dart half of the React component. APlStepperand aPlTimelinesay 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.semanticStepLabelsays the words, and it is a callback rather than a pair of strings forPlProgressLinear.formatValue's reason — there is noIntlin the framework, and pullingpackage:intlin 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 aPlButtonor aPlIconButtonunderneath depending on whether the words are drawn.labelis required and is always what a screen reader hears, whether or notextendeddraws it: an icon-only button with no name is the defect this pattern ships with everywhere else.While
floatingit is aPositionedDirectional, so it belongs in aStack— which is what a screen's body usually already is once anything floats over it — and the corner isstart/endrather 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
GlobalKeyrather 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 callsScrollable.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.
-
PlDataListandPlDataListItem. 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,MergeSemanticsdoes 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.
dividerrules 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.bareis 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, anImage.asset, aCustomPaint— andsemanticLabelis what a screen reader hears when there is noname. With anamethe mark is wrapped inExcludeSemantics, because a wordmark beside a picture of the wordmark is the product's name read twice.onPressedis the Dart shape of React'srender={<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 aPlTooltipis that the pointer can move onto it and what is inside can be pressed; what separates it from aPlPopoveris 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.
delayis 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
PlPopoverintointernal/wedge.darton 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 onRawScrollbarfrompackage:flutter/widgets.dart, because the framework's ownScrollbarlives inmaterial.dartand this package imports neither that norcupertino.dart. The thumb isPlassTokens.track, the same neutral ink aPlSlider's rail and a progress groove are cut in.heightis a parameter rather than an enclosingSizedBoxfor 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.maxHeightis the ceiling form.orientation: PlScrollAreaAxis.bothis two scrollables, one nested inside the other, and each bar answers only its own through anotificationPredicate. A horizontal bar that moved when the page scrolled down would be reporting the wrong axis.scrollbars: PlScrollbars.autoshows the bars while the pointer is over the box or the content is moving, which is what the React build's hover state does — aMouseRegionrather 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 aPlProgressLinearis cut into and deliberately not one of them. Progress is something advancing; a meter is something already known, sovalueis required, there is no sweep, and there is no indeterminate case to have a default for.thresholdsis what it exists for:PlMeterThreshold(from:, color:)bands where the highestfromat or below the value wins, read rather than walked so their order does not matter, andfromis 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.
SemanticsRolehas nometer, and claimingprogressBarwould 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.formatValueis a callback rather than an options object, for the reasonPlProgressLinear's is: there is noIntl.NumberFormatin the framework, and pullingpackage:intlin to provide one would be a dependency decision made on a consumer's behalf. -
Six widgets turn with the window.
orientationonPlPanes,PlTabs,PlScrollZone,PlTimelineandPlStepper, anddirectiononPlStack, all take aPlassResponsive— so a tab bar is a column on a phone and a row on a laptop from one argument.Resolved against
MediaQuery.sizeOf(context).widthinbuild, 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. -
maxWidthis responsive and takes an exact width, onPlContainer,PlHeaderandPlFooteralike.PlassResponsive<PlContainerWidth?>—PlContainerWidth.rung(PlassSize.lg)is a rung of the measure ladder andPlContainerWidth.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.nullstill means no limit, including as an entry, so a screen can be uncapped on a phone and capped frommdup.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.untilis exclusive, which is what lets it andfrombe 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
Offstagesubtree. That differs from the React build, which sends both halves and hides one withdisplay: 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.PlassBreakpointFloorcomes with it — the four rungs that have a floor to ask about.xshas nothing below it, sofrom: xswould mean "always" anduntil: xswould 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
ClipRectwith a clipper rather than anAlignwith awidthFactor, and the difference is the one this effect exists for: anAlignresizes 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.fromis physical —PlassSideis everywhere — andPlassAnimateMode.exitcloses from whichever edge it opened towards.fadeis off by default, alone among the effects that offer it: fading a reveal asks for two entrances at once.
Fixed #
-
A
PlMockupdrew itswallpaperover the screen instead of behind it. The decoration was handed to the screen container'sforegroundDecoration, which paints after its child, so anything opaque — aDecorationImage, 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 atDecorationPosition.foreground. -
A
PlImagereplaced 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. TheAnimatedOpacityis 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: aPlAccordion, aPlCollapsibleand aPlPillall 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 samePlassFoldthey 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 areAnimatedPositionedDirectionalnow and travel over the house duration.Duration.zerowhile 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 aTransform, 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.AnimatedRotationis 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 withRotatedBox, which turns the layout and cannot be animated — every other one is anAnimatedRotation, 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
PlCheckboxand aPlRadioGroupcut every animation they had, and the tick and the dot had none to begin with. Both widgets passedlit: markedtoplassStateFilter, andlitis 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 ownAnimatedContainerwas 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 byhovered, 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
PathMetricand cut at the fraction laid down, which is the same statement the React build makes with astroke-dashoffset. The dot grows out of the middle of its ring, as a box and not aTransform: 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
PlTabsbar with more tabs than room scrolls, rather than overflowing its box. Eight tabs in a 240px box was aRenderFlex overflowed by 474 pixels, and the documentation's answer — wrap it in aSingleChildScrollViewyourself — does not work: a scroll view around aPlTabsscrolls 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: aShaderMaskfades 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 withAlignmentDirectional, 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. Withbuttons="auto"and the defaultinlineplacement, 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 isdisabledand visible now, which is whatbuttons="always"already drew in the same position, so the two settings no longer disagree about what "nowhere to go" looks like. Whatautodecides is whether the strip has scroll buttons at all. Anoverlaybutton 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
reversiblePlSpoilerchanged 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
ghostPlTogglethat was off drew itself as one that was on the moment the pointer arrived. Its hover was the family'ssoft, andsoftis exactly what aghosttoggle 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 rungssolidandglasswere 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,PlOverlayandPlCommandPalette— everything built onPlassPortal, sheet and scrim together, since the portal owns the fade for all four.PlassTokens.durationSlowalready 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.
-
PlStackreplacesPlAvatarGroup, and this is a breaking change. A row of overlapping faces is one arrangement of a pile rather than a widget of its own, soPlStacktakes whatever it is given and never looks inside.max,totalandoverlapcarry over;direction,drop,front,scaleStep,opacityStep,overflowandringare 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 —EdgeInsetsasserts it is non-negative and so doesFlex.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×32horizontal, 32×120vertical, 120×72diagonal. 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.
ringtakes aBorderRadius?rather than abool: there a ring is a box shadow and CSS gives it the element's ownborder-radiusfor 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.
PlAvatarno longer reads one —sizeandcolorcome off the nearestPlassTheme, andshape,variantandelevationare the avatar's own.
1.2.0 - 2026-08-31 #
Added #
-
Five fields take a
hotKeysmap.PlTextField,PlNumberField,PlOtpField,PlComboboxandPlSelectnow 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 butonSubmitted. The chord is spelled the way a key cap is spelled, in the vocabularyPlHotKeysdraws:Modresolves per platform, andEsc,Return,CmdandOptionfold 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 argumentPlCommandPalettemade when it bound its own opener, with a private copy of the matcher to make it; that copy is nowinternal/keys.dart, andPlHotKeysreads 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, soEnterdoes not fire onShift+Enter; and a chord that matches is consumed, reaching neither the control's own keys nor the route above it.PlSelectis the one place nesting cannot settle — aFocusableActionDetectorbinds 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
PlDatePickercan stop at a month or a year.precisionisPlDatePickerPrecision.day,.monthor.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 aDateTime, 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 noformatValue(a newPlDateNames.monthYear, which swaps its two halves the waymediumalready does), and the footer's shortcut, which reads "This month" or "This year" rather than "Today" and brings two newPlPickerLabelsentries with it.minDateandmaxDateare then read at the same precision: aminDateof 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.shouldDisableDateis day-granular and is not consulted at all. -
PlCalendar. The grid aPlDatePickeropens, on the page. It is not a new calendar — it is the sameinternal/calendar.dartthe 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 nolabel,descriptionorerror.precisionis 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 nodensity, because padding on a grid of forty-two squares is what stops them being squares, and noreadOnly, 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.autofocusis 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 —namesandlabelsrather than alocalestring, and noname, because a Dart form is not an HTML one. -
PlStepperandPlStep. A process the reader is moving through, and where they are in it. It draws the same rail aPlTimelinedoes — and shares it: the bullet, the connector, the three states and the status arithmetic moved into a newinternal/steps.dartthat both widgets read, because a haloed bullet must not mean two things and two copies of a painter are two copies that drift.PlTimelineStatusandPlTimelineConnectorare 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 opaqueWidget. That also settles the sharp edge the React build has to warn about, since there is no wrapper to hand it.linearis 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.optionaltakes aWidgetrather than abool, because there is no default string to fall back to: the package ships no translations. The current step is markedselected, the nearest thing the framework has toaria-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.PlTabletakes its columns the same way and for the same reason.expandedandselectedareSet<String>, both controlled like every other input here, and each callback hands back the whole set rather than the one id that changed.selectionissingleby default;nonemakes the tree a browser rather than a chooser — rows still expand and a press still reports throughonItemPressed, but nothing stays lit.children: []andchildren: nullare 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'sFocusNodebut the current one carriesskipTraversal, which takes it out of the Tab order while leaving it in the focus tree, so the arrows can still reach it. -
PlImage. AnImageis 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 isratio, 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 thesemanticLabel, 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 changedimagestarts again rather than inheriting the last one's success.imageis anImageProviderrather than a URL, because that is the shape every source has in common.previewopens the picture over the page in aPlOverlayand is off by default — a picture that grows when you press it is a promise that there is more of it to see. Without aratiothe widget is the picture's own intrinsic size, which isImage'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 whycoloris what tells them apart:secondaryis "nothing here yet",dangeris "something went wrong",successis "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'semptytakes a widget, and this is the widget it was waiting for. The part worth getting right is the way out, and it is the reasonactionsis a slot rather than an afterthought: a screen that says "No projects" and stops is a dead end.actionsis aList<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 threeTexts 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 byimprovesWhenand 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.valuetakes 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'sNumberFormatis the app's. The one number the widget does write isPlStat.formatChange, at most one decimal and a sign on a rise. The figure isFlexibleand 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.controlleris aScrollController, or thePrimaryScrollControllerwhen it is left out, which is what aListViewwith 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 nofloatingand no equivalent of the web'sposition: fixed— where the button goes is the caller'sStack. -
PlConfirmProviderandPlConfirmProvider.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 onePlToastProvidermade: 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, aPlModalkept in the tree beside it, and the work after the answer torn in half across a callback.alertis 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 itsfinally. AndPlConfirmProvider.ofasserts outside a provider rather than answeringfalse, because a silentfalseis a delete button that quietly does nothing. -
PlPopconfirm. A question asked where it was raised. The difference fromPlConfirmProvideris 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.colordefaults todangerhere and toprimaryon aPlButton, and that is not an inconsistency — nobody reaches for a popconfirm to ask whether to save.onConfirmreturns aFutureOr<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. -
PlassThemesets the defaults once. Seventy-seven widgets resolvedsize,coloranddensityfrom a literal in their own constructor, which meant an application whose controls aresmsaid 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 asize: PlassSize.lgon one button still wins inside aPlButtonGroupinside a compact application. The date vocabulary comes with it —names,labelsandweekStartsOnreach all five date widgets, which is what alocaleis here, since the framework ships noIntl.Use
PlassTheme.merge. The plain constructors replace the defaults in scope;mergekeeps the ones above and the brightness with them, which isDefaultTextStyle's own bargain and is there for its reason — anInheritedWidgethas no context to read an ancestor with, so merging happens where there is one.variantandelevationare deliberately not inPlassDefaults, and the absence is the load-bearing part.variantnames what a surface is made of, and aPlButtonissolidwhile aPlCardisglassbecause that is the arrangement rather than an unset default;elevationis per-widget semantics for the same reason, a control resting on the sheet at1and a field cut into it at0. 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:PlAlertdecided whether it was a live region by reading its unresolved colour, so an alert whosedangercame from a theme announced itself as though it were a note; andPlSidebartouched a lazily-initialised width fromdispose, which is an inherited-widget lookup on a deactivated element.
Fixed #
-
A
PlPillcould not stand anywhere it was not given a width, which included the arrangement its own documentation describes.Row, and aPositionedthat 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 wasCrossAxisAlignment.stretch. Stretching against an unbounded constraint asks for a tight infinite width, so instead of a wide pill you gotBoxConstraints forces an infinite widthand a cascade ofRenderBox was not laid outbehind it — an error naming aRenderConstrainedBoxand nothing you wrote.The
stretchwas right and its reason has not changed: the row and thedetailspanel 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 whatstretchwas 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
Wrapof 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 aPlChip. This is a Flutter-only bug: the React pill isinline-flexand 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
EdgeInsetsDirectionalandPositionedDirectionalalmost everywhere, and "almost" is the failure: a physicalleft: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.PlSliderwas 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.PlPillput the air meant for the leading glyph on the trailing side.PlAnimateMarqueeran the strip off the wrong edge, delivering the words backwards.PlScrollZonemeasured 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 byPlDateRangePickerandPlCalendar— 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 underlib/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 aPlassSide, 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 inPlAccordion,PlCollapsible,PlSpoilerandPlCommandPalette. -
A chord ending in
Spaceor a numpadEnternow 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; andNumpad Enterfolded 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 newhotKeysmaps, 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
AnimatedContainertweened 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,colorandelevationare 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.dartfollowinginternal/button_group.dartone component over, which is also whyPlAvatar's five axes are now nullable:nullis "this avatar did not say", the stack answers next and the house default last. Two things are said differently. The faces are aList<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 margin —EdgeInsetsasserts it is non-negative — so the overlap is aStackof 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 ownsizerather than layout. The ring is the same idea as the stylesheet's and the opposite construction: a FlutterBorderis 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 ownsurface. 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-rolledStackgoes with it.PlMenubar. The strip of words at the top of an application — File, Edit, View — each of which opens the same menu aPlMenudoes, with a different trigger. What makes it a bar rather than a row of menus is that it is oneSemanticsRole.menuBarwhose words aremenuItems, 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 controlsFile Edit Viewwould 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 incomingvaluere-seeds the model only when it means something different, compared as a colour rather than as a string. The conversions are a newinternal/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 aPlMenu— a menu is a short list in one place and every row is visible before you look for it — and not aPlCombobox: 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, andkeywordsare matched and never drawn. Three things are said differently.openis 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 anEditableTextconsumes 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 reasonPlTransfer's is: Dart's core has noString.normalize. The opener is read with the sameMod-aware vocabularyPlHotKeysdraws, so the cap on a row and the key that works cannot drift apart — and a row's ownshortcutis 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, anddisabled. 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 owndisabled.PlForm. A form that knows which of its fields is wrong: the stack, a submit that collects everyFormField's validity at once, anderrors— 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'snameputs 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. SoonSubmitreports that the form is valid rather than a map of values, anderrorsis read explicitly throughPlFormScope.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 aGlobalKey<PlFormState>or from anything inside throughPlFormScope.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, claimingSemanticsRole.navigationand 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 nohrefanywhere, because there is no navigator in this package and no address to resolve:onPressedis where a destination is decided, exactly as onPlTextLink. 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 controlledvaluewould be one that could never be closed from outside — which panel is open is the pointer's state rather than the app's, andinitialValueis 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'slabelis aStringrather 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, socafefindsCafé, and Dart's core has noString.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.PlToggleandPlToggleGroup. 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: aPlButtonat 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 aPlSegmentedButton'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 samePlassButtonGroupScopeaPlButtonGroupdoes, 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 inwidgets.dart, and a roving focus implemented badly is worse for a keyboard reader than the platform's own traversal —PlSegmentedButtoncarries the real thing where a value needs it.PlSidebar, and thePlSidebarTriggerthat 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 claimsSemanticsRole.complementaryand 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 aLayoutBuildersees the constraints it was handed; giving the sidebar its owncollapseBelowis 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 notitledraws itssemanticLabelas the heading, because aPlDraweris named by what it draws. And the resize handle is aSemantics(slider: true)with the width as its value rather than arole="separator"with anaria-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 aValueNotifierrather than insetState, 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 claimsSemanticsRole.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 tocontentinfoat 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 andPlHeader: 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, forPlCard'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.sizeis a floor and not a height (mdis 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.brandandactionsare lists, because a slot is a row and the gap inside it is the bar's to decide — the same shapePlToolbargives its two ends. There is noposition: afixedelement has to span something, and a widget goes exactly where the screen puts it. And there is nobannerlandmark to claim, so a named bar is aSemanticsRole.regionand 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 butSemanticsRole.mainaround what it was given, and leaves the gutter and the measure toPlContainer— 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 astickybar sits across the top of the window without taking anything out of the flow, and aColumnhas already done that arithmetic — the band below the header is exactly what the header left. AndcollapseBelowis checked against this layout's own width rather than the window's, because aLayoutBuildersees the constraints the layout was handed: an app shell inside a pane collapses when the pane is narrow, which a media query cannot say.nullis the Dart spelling of "no floor was named". There is noscrolland noheight— the layout fills the space it is given and what scrolls is whatever is put inchild— and no skip link, because a link to a fragment needs fragments and a traversal order that is the document's.PlTextLinktakes astartIcon. Its one icon slot was the mark after the label, and it is not a general one:iconis about the link's destination and has an opinion, drawing the arrow for anexternallink unlessshowIconsays otherwise. There was nowhere to put the other kind — a favicon, a file type, a lock — so a caller built their ownRowand paid for the gap by hand.startIconis 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
PlScrollZoneanswers the wheel. A horizontalScrollablereads 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.wheelis 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 thePointerSignalResolverrather 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'sshape,variant,size,colorandelevationare nullable. Each of them used to carry its default on the parameter; now the default is applied when the value is resolved, andnullmeans this avatar did not say — which is what lets aPlAvatarGroupanswer for a face that stated nothing while a face that stated something keeps it. The arrangementPlButtonalready has inside aPlButtonGroup. 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.truncateputs 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.buttonPlacementnow defaults toPlScrollZoneButtonPlacement.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.overlayis unchanged and is still what a shelf of pictures wants.
Documentation #
- The
READMEno 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 reached1.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 underlib/changed. - The
READMEopens 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 neithermaterial.dartnorcupertino.dart. One paragraph describing the design itself takes its place, shared word for word with the React package and the repository's ownREADME, 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 behindExcludeSemantics, 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 aTextout 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.charactersknows 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 positionalStringrather 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 — aStack, 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:intervalis counted from the moment a line arrives rather than from the start of the cycle, so raisingdurationdoes not quietly eat the reading time. Passingindexmakes it controlled and takes the timer away outright — a controlled headline is somebody else's clock.riseis adouble?wherenullis one line's own height, the same tradePlAnimateSlide'sdistancemakes; there is noalternate, 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 anUnconstrainedBoxwithClip.hardEdgerather than a plain clip — a clip alone clips the paint and leaves theRenderFlexasserting that it overflowed. There is no percentagetranslateto 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:speedis 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 istheld at zero — the content standing where it started.PlassAnimateRunlearned 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 aSweepGradienton a layer behind the child — first in aStackwithclipBehavior: Clip.none, inset by a negativespread— 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 ownGradientRotationrather 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.sizehas 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 avisiblelist 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 adisplay: flexon the container, soorientationandspacingare what aclassNamewould have done, and anything more elaborate belongs inside one child — which also makes that whole arrangement one step of the stagger.pausedgoes 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.repeatis left atnullhere, the value that never stops, because a single blink is a flicker and nobody asks for a flicker.minis 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 one —fromalone is an arrival, andfrom: 0, to: 360with anullrepeat andCurves.linearis 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 nodistanceit 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:distanceis adouble?in logical pixels andnullmeans its own size, because a fraction of a widget's own size is whatFractionalTranslationalready means and there is no CSS length to spell.fromis 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 asPlAnimateGrowat 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 noorigin: 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 isoriginand 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.originis anAlignmentrather than a CSStransform-originstring — the framework already has the type, andAlignment.topCenterreads better than'top'. One other thing is said differently: withfadeoff there is noOpacitywidget at all rather than one sitting at1, 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 wholePlAnimate*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.dartis the Dart half of the React package'sinternal/animate.tsand keeps the same two ideas. Waiting is a held first frame, not a hidden widget: avisiblefade sits att = 0, faded out and taking its space, until it is scrolled into view — which is whatfill-mode: bothplus 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 anexitfade stays faded out. Four things had to be said differently.durationanddelayareDurations rather than milliseconds, because the framework already has the type.easingiscurve, aCurve.repeatis anint?wherenullnever stops — there is no'infinite'to write, and-1would be a sentinel a caller has to look up; it is the same tradePlProgressLinearmakes with a nullvalue. AndmodeisPlassAnimateMode.enter/.exit, becauseinis a reserved word in Dart.trigger: visiblewatches the nearestScrollableinstead of anIntersectionObserver, 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 importpackage:flutter/material.dart,package:flutter/cupertino.dartordart:mirrors, andtest/package/framework_surface_test.dartfails 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 aMaterialAppreachesThemeData, 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.dartis the on-demand measurement the numbers come from. PlDateTimePicker. A day and a time in one popup — the calendar isPlDatePicker's and the columns arePlTimePicker'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 withPlDatePicker:minDateis 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 aminTimeof 09:30 the hour9stays available — it contains allowed minutes — and the minute column is where00through25grey out. Two things are said differently.hour12is a plainfalserather than "whatever the locale does", because there is noIntlhere to ask, and the words it uses when it is on are thePlDateNames.am/.pmthat already come with the month names. And the chosen row is brought into view by computing an offset and jumping its ownScrollController, neverensureVisible, which walks every scrollable ancestor and would move the screen behind a popup that has only just been positioned.closeOnSelectis false — a time is two answers — so the footer carries aDone.PlDateRangePicker. A span between two days:PlDatePickerwith 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.valueis a non-nullablePlDateRangewith aPlDateRange.emptyfor 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 anIntrinsicHeightaround its row and anIntrinsicWidtharound 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 aPlTextField'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 getsIntlfrom the browser, so a BCP 47localestring produces every month name and the order the header writes them in — the framework ships nothing of the kind, and a package that pulledpackage:intlin to fill the gap would be making a dependency decision on its consumer's behalf. So the words arrive as aPlDateNames: English by default, so a picker works with no setup at all, and three lines ofDateFormatfor an app that already hasintl.formatbecameformatValue, 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 isJulyin 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 aPlTextField's to the pixel, exactly asPlSelect's trigger is. Two things had to be said differently. Holding a set is a second constructor,PlCombobox.multiple, rather than amultipleflag: one widget with a flag would have to hold both shapes of value and neither would be typed. AndallowCustombecameonCreate, aT Function(String)— React can default that permission totruebecause a value there is always astringor anumber, and here it is aTthat 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 takesPlProgressLinear's parameters unchanged except forsize, 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 aShaderis built by hand: a stroke takes a shader rather than a decoration, so the 135° sweep every other surface gets fromPlassCssGradientis asked for directly. Indeterminate, it draws a fixed quarter-arc and turns — the same exception a spinner already has — and slows rather than stopping underMediaQuery.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 aPlSlider's rail and aPlSwitch'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.formatis a function rather than an options object: there is noIntl.NumberFormatin the framework to hand options to, and a package that pulledpackage:intlin to provide one would be making a dependency decision on its consumer's behalf. And the indeterminate segment travels on a directionalAlignmentrather than oninset-inline-start— neither is a transform, and both run the other way under RTL without being told. It is one merged semantics node carryingSemanticsRole.progressBar, orloadingSpinnerand 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.childrenis aList<Widget>rather than onechild, 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 —EdgeInsetsasserts it is non-negative — and the alternative is aTransform, which this library does not put on a control. Both arrive at one hairline per seam. The axes onPlButtonandPlIconButtonbecame 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 —startandendpinned to their ends andchildtaking 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 thesize/densitypadding every other surface uses. There is noposition— afixedelement has to span something and a Flutter widget goes exactly where the screen puts it — so all that is left of it isrounded, 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 behindExcludeSemantics,ExcludeFocusand anIgnorePointer— the three thingsinertsays over there.PlPopover. A sheet that opens beside the thing that opened it, onPlassAnchoredPortal— 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.triggeris required here where React makes it optional: a browser can position a popup against the viewport with no anchor, and aLayerLinkhas nothing to follow without one. There is nomodaland noalignOffset— 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 noposition: afixedelement 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:overlayopens over the screen onPlassPortal— the same layer a modal and an overlay stand on, so one opened over the other shows no seam — andinlineis simply part of the layout. There is novariantand noelevation, because a panel that has taken an edge of the screen has already answered what those ask. There is notriggereither: the app opens it by settingopen, and the button that does it is the app's own.PlCollapsible. One section that folds, standing on its own — the same fold aPlAccordionis a set of, with nothing to coordinate with. The panel is aSizeTransitionclipping a body that never changes size, so nothing is transformed and no text is resampled.triggerbecomestriggerBuilder, 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.keepMountedhas a sharper reason here than there — a FlutterStategoes 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 aPageView— Flutter's own scrolling with snap points, so a swipe is the platform's and not a gesture handler imitating it. It gainsaspectRatio, which the React build has no need of: a browser's strip is as tall as what is in it and aPageViewlays every page out at the viewport's size.autoPlayis 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.sizeis 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, becauseoverflow: hiddenis 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 manylinesas you ask for. The mechanism is an ordinarySingleChildScrollViewand 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.spacingis a length here rather than a step on the React build's spacing scale,controlleris offered because Flutter drives a scroll view with one, anddragputsPointerDeviceKind.mouseback intodragDevices— 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.fitis nullable here andnullby default, where React defaults it tocover: in a browserobject-fitonly ever reaches a replaced element, and aFittedBoxreaches 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 aResizeObserverbecause CSS measures itself, and aLayoutBuilderis handed the extent on every layout pass.PlPaneSizeis two constructors where React has anumber | stringunion, a handle is slider semantics because Flutter's tree has no separator role and novaluenow, 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 sealedPlMenuEntryhierarchy the menu is handed. Focus stays on the trigger while the popup is up, which is whatPlSelectdoes; the arrows,Home,End,Escand 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 noPlContextMenu: 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 — afixedelement 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 wayPlSegmentedButton'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'slabelis aStringrather 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 noposition— a Flutter screen has no page scroll to opt out of — and nohref, soonChangedis where a router is called. The current destination is marked selected, which is the nearest thing toaria-currentthat 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 thanFilteringTextInputFormatter, 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 aCustomClipper, 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.readOnlydrops the gestures entirely for one image node.PlIconButton. A disc with a glyph in it, and almost none of it new code — everything isPlButton's.labelis 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.PlButtongains one escape hatch,borderRadius, which is the door the React build already has through an inlinestyle.PlGridandPlGridItem. 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 aWidgetis 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: falsescrolls 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.maxWidthis a nullablePlassSizewhere 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 aremagainst 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.levelsets the size and whether the line is announced as a heading; there is nosizebeside it, because asizewould let a caller ask for anh1atxs.PlTypography.richis the span form.PlAvatar. A picture, or the initials, or a silhouette — never an empty box.imageis anImageProviderrather 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.contentandcountare two parameters rather than one, becausemaxandshowZeroonly 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.onPressedis whatrender={<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, sorole="alert"againstrole="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 realPlButton. There is nogetPageHref: nothing crawls a Flutter app, so the row is buttons andonPageChangedis where a router is called.PlTextField. Single- or multi-line, with the label, the description and the error as part of the component. Built onEditableTextrather than on aTextField: 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,PlRadioGroupandPlSwitch. 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 nodefaultCheckedanywhere in the package. A radio group is generic in its option's type, sovalueandonChangedare checked rather thandynamic, and it owns the roving tab index: exactly one option is in the tab order, the rest are wrapped in anExcludeFocus, and the arrow keys move the choice with wrapping.PlTextLink. A line under a word, and nohref: Flutter has no navigation of its own, so where a link goes is the app's andonPressedis 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.PlHotKeysandPlKbd.Modresolves per platform fromdefaultTargetPlatform, 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.PlListandPlListItem. The list is the sheet and the rows are what is on it, sosize,density,coloranddividersreach a row through anInheritedWidget. A row'sactionis a separate focus stop from the row itself.PlBreadcrumbandPlTimeline. Both take their members as descriptions rather than widgets — Flutter's own idiom, the oneDataColumnuses. 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. AWidgetis 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.lengthwins over a tight parent — a divider very often sits in aColumnwithcrossAxisAlignment: stretch, where a bareSizedBoxwould 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 aGradientTransformrather 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 aStringrather than a widget tree, which is the one real difference from the React build: aWidgetis 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 — throughIconTheme, throughDefaultTextStyle, and by the box it is laid into — so anIcon, aCustomPaintthat reads the ambient theme and a bare character all come out the same size.coloris nullable and defaults tonull, which is how "inherit" is spelled in a language with no such keyword.PlAccordionandPlTabs. Both take their members as descriptions rather than widgets, and both fold the panel into the member that opens it — aPlAccordionItemcarries its body, aPlTabcarries its panel, so there is no second list to keep in step. The accordion'svalueis aSet<T>even withmultipleoff, 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 ownTable, 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:cellis required here, because Dart has norow[key]on an arbitrary type. Width comes in two forms rather than one — a length in pixels, or aflexshare of what is left, which is what the React build'swidth: '30%'already meant.maxHeightcaps the grid and the rows scroll inside the sheet past it, with the caption left above what scrolls.stickyHeaderpins 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 oneTablewith 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 aPlTextField's to the pixel, and what sits on it is a real numeric control: the arrow keys and the steppers move bystepwith 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.formatis two functions rather than anIntl.NumberFormatOptions— one writes and one reads — because there is noIntl.NumberFormatin 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.PlOverlayandPlModal, and the layer under both. A sheet that takes the page away lifts itself into the nearestOverlay— 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 notriggerand noPlModalClosehere. 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: aLayerLinkkeeps 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.openis abool?, andnullis 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.PlTooltipProvidershares one delay across a toolbar.PlSelect. The trigger is aPlTextField's shell wearing a chevron and the list is aPlTooltip'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. APlToastProviderwrapped once around the app, andPlToastProvider.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 noOverlay: the provider is already above everything it has to cover, and nothing in the strip is told to ignore the pointer because anAlignhit-tests its child and not the room around it. Showing an id already on screen changes that toast in place,showFuturemakes 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, soonBrowseis where the app's own picker runs and everything after that is the component's: the rules, the list, the removal and the box.acceptis 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 — andmaxFilescounts 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, sodraggingis 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.yamlclaimedplass-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.
.pubignoreexcludedexample/, 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 anexample/README.mdthat is a whole running app in forty lines, since the gallery's ownmain.dartopens 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,densityandelevationoff the shared vocabulary, plusstartIcon,endIcon,loading,readOnly,disabled,fullWidth,semanticLabeland the usualfocusNode/autofocus/onLongPress.onPressed: nulldisables 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 toMediaQuery.platformBrightnesswhen 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 inPlassColorplus its colours.- The shared vocabulary —
PlassSize,PlassColor,PlassVariant,PlassDensity,PlassElevation,PlassOrientation,PlassSide,PlassAlign,PlassCorner. Anmdis 40px on everything, and it is the same 40px the React package means. - Nothing from
material.dartorcupertino.dart. The package is built onpackage:flutter/widgets.dartalone, which is what lets it sit inside a Material app, a Cupertino app or a bareWidgetsAppwithout 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.blurRadiusconverts 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
solidgradient.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 waylinear-gradient(135deg, …)specifies instead. - The glass hairline.
BoxShadowonly 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.