Review release notes for Pluma components, icons, tokens, and the MCP package.
Changelogs for components, icons, mcp, and tokens. These, and other releases, can also be found on the Pluma repository
bar takes layout: 'nested', for series that contain one another rather than adding up to a total — sent, delivered and opened. Each series is drawn inside the one before it on the same category and painted over it, at its own value, with one legend and one shared tooltipbar takes a thickness fraction, so a bar can be narrowed by a share of the room its category leaves it rather than by a pixel count — three bars at decreasing fractions read as a funnel at any plot width, where a pixel ladder tuned wide collapses the innermost to nothing narrowstackOffset: 'share'. legendValueFormat is called with what the drawing allocates, so hiding a series re-proportions the numbers the way it re-proportions the slices or the columns, a hidden series reads as zero, and a gauge measures against the total it declared. Marks drawing their own units are unchanged, since their geometry stays put when a series is hidden.arc takes an outerRadius fraction, so a ring can be sized apart from the surface it is drawn on — a legend column that needs a row per series no longer sets how big the donut ischartSelectors publishes the selectors for the shapes a chart paints, so a test can reach into the plot without hard-coding renderer class names that move between releasesbar and area take stackOffset: 'share' for a hundred-percent stack — the value axis reads in percent and each row keeps its own number, so the tooltip reads out bothbar and area take a stackOrder, so a stack can read largest-first without reversing the rows — the palette still follows the order the series are declared in, which reversing the rows by hand silently changesChart<TDatum> types the row behind each focused point, so a custom tooltip can read a field off it without a type assertiontickCount now works on a category axis too, where it used to be dropped. A category axis with withGrid: true draws a line per labelled tick rather than per category.sequential-teal-1 was APCA Lc 7.5 against the dark surface. Both modes are now anchored on step 3, the single-series pick, at Lc 60 against white and Lc 55 against the dark surface, with each hue's remaining rungs spaced from there. Categorical takes the same color as the single-series pick. Adds 18 palette steps.abbc8d4: Chart: add categoryInset and maxThickness to the bar mark, for trimming a bar on both sides and for capping how wide it is painted. Both apply per bar, so under layout: 'grouped' they act on each bar in the group.
Two defaults move with this. The gap between categories widens from a fifth of each category's slot to a third, and bars are drawn with a 2px corner radius instead of square — pass radius: 0 to square them off again. A stack is now rounded as one bar: the two ends of the column are round and every seam inside it stays square.
b13e08f: Chart: either end of an axis domain can now be 'auto', which holds the other and leaves that one to the data. domain: [0, 'auto'] anchors an axis at zero without also pinning a maximum.
Two fixes for charts with several vertical axes: a pinned range no longer skips the pass that picks the tick count for the whole set, and the ticks of a pinned axis are written at the step's own precision, so a [0, 6] over six heights reaches a consumer format as 3.6 rather than 3.5999999999999996.
Pinning one axis on such a chart can now resolve to a different tick count than the six it used to be fixed at.
d0ed060: Chart: the dot a line grows at the focused point now carries a ring in the line's own color at a quarter opacity, and arrives with motion — the dot fades in and the ring grows out of the point. The defaults draw a 5px dot inside a 4px band, replacing the 4px dot with a 1.5px surface-colored stroke.
Size it per line mark with focusDotRadius, focusDotRingWidth, focusDotRingColor and focusDotRingOpacity.
6490edb: Chart: a shared tick step can now land on 3, so a multi-axis chart reaches counts like 0/300/600/900 instead of climbing to 1200. Ticks on existing multi-axis charts may move as a result; charts with a single vertical axis are unchanged.
7df8025: Chart: tooltip rows now read in the order the marks and series are declared, rather than by where the focused points sit on the plot. Pass tooltipSeriesOrder="position" for the old ordering.
position: 'right' stays on the right when it is the only axis left. Hiding the last series off the left-hand axis used to walk the surviving guide across the plot; a chart declaring a single right-hand axis drew it on the left for the same reason.bar mark that is a single series now names itself in the tooltip. One bar per category gave the grammar no reason to read its color as the series, so the tooltip labelled the row — and its aria-label announced — the mark's generated id (bar-y-1) instead of the mark's seriesName. Lines were unaffected, which made it show up on exactly the documented mixed chart: bars for the volume, a line for the rate.b6ff7eb: Select: add SelectPalette, the dropdown surface, with optional header, categories rail, detail pane and footer regions. Categories take tooltip/tooltipPlacement and can be grouped with an optional collapse.
Icon slots across Select and DropdownMenu gain iconSrc, iconColor and iconSize, plus trailingIcon* on DropdownMenu items.
A palette with a rail or a detail pane holds a fixed height so it doesn't resize as the active category changes — 480px with a header and footer, down to 360px with neither — and traps Tab so it can't be dismissed by tabbing out. It also opens with the caret in the search input rather than on the close button. Override the height with selectPaletteHeight.
Existing dropdowns are unchanged except for four things: they now cap at 480px tall (raise it with unsafe_popoverSize), the search header is plain rather than grey with 12px padding (selectMenuHeaderBackgroundColor, selectMenuHeaderPadding), the first option is highlighted on open, and the highlight no longer clears when the pointer leaves the list.
zones and zoneAxis to the line mark, for stroking part of a line differently — a dashed stretch where the data is still provisional, or a color above a threshold. A zone is a range on the axis, bounded by from, by to, or by both. The line stays one series drawn as one fitted curve, so an area under it keeps a matching edge. A colored zone keeps its color under focus, takes a hoverColor where its color has no vetted hover twin, and lends its color to the tooltip's swatch.c8269a6: Charts: add the dot mark and decorative
dot draws a dot per row, for points a reader is meant to notice — where a line
turns provisional, an outlier worth naming, readings too sparse to join up. It
takes radius, and ringColor / ringWidth for a ring that punches the dot out
of whatever it lands on.
decorative wraps any mark so it still draws and still contributes to the
scales, but owns no chart points — which takes it out of the focused group, and
so out of the tooltip, off the keyboard cursor's path, and out of onSelect and
onFocusChange. The distinction a chart needs whenever a mark restates something
already on the plot: a marker on a line, a threshold, a target.
Both go through marks like every other mark in the grammar.
The docs Ember renderer also registers @customerio/pluma-components/css/theme-tokens for embedded snippets, which the React one already did — so an Ember example can reach a token the same way.
7de7c25: Chart: adds a margin prop — one number for every side, or the sides to set — for the space around the plot. A side that is set replaces the room the chart would have measured out for the labels drawn on it rather than adding to it, so it has to be wide enough to hold them, and a single number has to clear all four sides at once. Sides left out keep what was measured, and a legend beside the plot keeps the column it reserves on that side.
definition is now laid over the definition built from the chart's other props rather than replacing it, so anything the props don't reach can be set without rebuilding Pluma's defaults. Options merge all the way down; arrays, scales and functions replace what they land on. Passing a function instead hands you Pluma's definition and takes back the one to draw.
A function is the only form that replaces: an object inherits every key it doesn't mention, x, y, theme, motion, focus, focusRing, maxFocusDistance and tooltip included. A chart passing a whole buildChartDefinition result should hand it over as a function — definition={() => myDefinition} — which matters most for a radial or sankey definition, since those omit the cartesian axes an object form would inherit.
7845f49: Chart: adds withExport, which puts a download menu in the chart's top corner offering the chart as a PNG, a PDF, or a CSV of the numbers behind it. exportFormats narrows and orders the menu; exportFileName names the files, which otherwise take their name from ariaLabel. The code that writes a file is loaded on demand, so a chart that offers a download costs nothing until one is asked for.
b578ead: Chart: bumps @tanstack/charts to 0.14.0. Pluma's chart props are unchanged. An app that imports @tanstack/charts directly should upgrade with it.
A line and the area under it are now swept on from the left when a chart animates, rather than revealed behind a clip the size of the plot. The reveal cut both marks at the top and bottom of the plot — a stroke is centred on its geometry, so a series touching either end of its domain had half its width shaved off until the entrance finished. Bars still grow from the baseline, and an arc now sweeps through its angle instead of fading in.
withAnimationdata-pluma-chart-tooltip rather than the renderer's ts-chart-tooltip class. Removes the chartTooltipMotion style export, whose class no longer exists now that Popover owns the tooltip's fade; the tooltip's own class is chartTooltipSurface.PageHeaderExtra slot for page-level content between the header's description and its tabs, inside the content band. A header without the slot lays out exactly as before. withDivider on the slot closes the header with the line a header would otherwise need an empty PageHeaderTabs to draw.PageHeaderExtra slot in the design docs — anatomy list and both anatomy diagrams.DataTable: add columnsSettings.maxVisibleColumns, capping how many columns the columns menu will let a reader select. Once the cap is reached the unselected checkboxes are disabled and a tooltip explains why, mirroring how the last visible column's checkbox is already disabled at the lower bound.b51a762: Cap how far autoPositionedLabel moves a label to clear the line. A label that would have to travel more than a third of the plot's height now stays beside its own point and sits on the series instead — on a short plot whose line runs its whole height there was never any clear space to reach, and text that far from what it labels reads as a stray number. resolveLabelPlacement takes maxPush to retune the limit and returns isOverlapping to say when it applied.
06a0eed: Draw Chart lines at 2px by default, replacing the charting grammar's off-scale 2.25px, and drop the narrower widths the sparkline examples set by hand. Lines given an explicit strokeWidth are unchanged.
Lines also stop being stroked vector-effect="non-scaling-stroke", which Chrome paints under-width on high-density displays, so a 2px line reads as about one. Temporary, and removed once the Chrome bug is fixed.
Monday, July 20) and the month with its year when it is monthly (January 2026); a monthly axis writes Jan '26 rather than the bucket's day. The tooltip's series swatch and its header spacing now match the rest of the product, and every figure and date the chart formats itself is written in en-US so a chart reads the same wherever it is opened.keepColumnHeadersDuringSelection to DataTable. When enabled, selecting rows no longer covers the column labels with the {n} selected overlay — for tables where selection is persistent state rather than transient bulk actions. Defaults to false, so existing tables are unchanged.@tanstack/charts to 0.9.0 and drops @tanstack/charts-scales, whose
scales now ship from @tanstack/charts/scales/*. Pluma's chart props are unchanged
except definition, which now takes a DomChartDefinition — the definition type
with its tooltip host pinned to dom, which is what buildChartDefinition returns.
An app that imports @tanstack/charts directly should upgrade with it: 0.8.0 renamed
ChartBehavior to ChartControl and the definition's behaviors to controls,
and transform accessors now take (datum, { index, data }).line and area marks take a curve option — 'smooth' rounds the
joins into a monotone spline, for a filled trend in the shape of Highcharts'
areaspline. The curve passes through every row and never overshoots, and a
stacked area keeps its stackGap under one.withHoverColor (swaps to -hover data-viz twins), withUnfocusedDim, a focusMode prop ('group' | 'nearest'), color accepting a DataVizColor object, and new --pluma-c-chart-series-*-hover / --pluma-c-chart-crosshair-color CSS variables. Bumps @tanstack/charts and @tanstack/charts-scales to 0.7.2.withLegendHoverFocus lets pointing at or keyboard-focusing a legend entry highlight that series, driving the marks' existing withHoverColor and withUnfocusedDim options across the whole series. No tooltip or crosshair is drawn.legendPlacement moves the color legend to the 'bottom', or into a
column beside the plot with 'left' or 'right'. legendValueFormat writes a
number next to each series' name — its total, its share of every series, or
whatever the formatter makes of them.yAxis now accepts an array of axes for quantities in units that have no scale in common. Give an axis an id and point a mark at it with yAxis: '<id>'. Guides are drawn down the left and stack outward in declaration order; position: 'right' moves one opposite. Every axis is drawn at the same tick heights over one grid, and where a bar or an area sits on an axis past the first, they all share a zero.arc mark for pie, donut and gauge charts, with innerRadius, sliceGap, cornerRadius, a partial angular range, and total / withTrack for gauges. Adds a chart-arc-track-color CSS override variable.sankey mark for flow diagrams. It takes one row per link with source, target and value accessors and derives its nodes from them, with nodes for names and fixed colors, nodeLabel, align, nodeWidth, nodePadding, nodeRadius, linkOpacity, withNodeLabels, valueLabel, valueFormat, and the usual withHoverColor / withUnfocusedDim. Pointing at a ribbon focuses that ribbon and opens a tooltip naming both of its nodes.seriesName names a mark that is a single series, so it can be labeled in
the legend without series: () => 'Delivered'. series wins when both are set.variant prop. variant="sparkline" draws the marks alone — no axes, no ticks, no grid — and falls back to a 40px height, for a trend small enough to sit beside the number it qualifies. Everything it sets is a default, so height, width, xAxis and yAxis still override it.withLegend now renders an interactive legend. Each entry is a button that
takes its series off the plot and puts it back, with the axes recomputing from
what's left; hiddenSeries / onHiddenSeriesChange control the hidden set.
Upgrades TanStack Charts to 0.7.2 for the legend it provides.withAnimation, which draws the chart in on mount and tweens between shapes when its data changesarc's sliceGap is now a width in pixels rather than an angle in degrees, so the gap between slices measures the same at the hole as it does at the outer edge. An angular gap subtends more the further out it is read, which left a donut with visibly more air between its slices outside than inside.bar's orientation: 'horizontal', and fixes the axis
inference behind it. A horizontal bar transposes which channel defaults to the
row index and which to the row itself, so a mark built without an explicit x
or y had its tick formats and date handling inferred off the wrong axis.withUnfocusedDim defaults to true for sankey marks. Sankey marks take orientation: 'vertical' to run the flow down the page instead of across it.x, y,
series and key given a function were called with the underlying grammar's
{ index, data } context instead of the row index ChartAccessor documents, so
a mark using one drew nothing. Hiding a series through the legend now also keeps
those accessors on the row's original index, rather than renumbering the rows
that stay.b4f6015: Add area and autoPositionedLabel marks to the alpha Chart, plus domain on an
axis. area fills to the value axis' zero and takes withGradient for a fill that
fades out at zero rather than at the bottom of the plot. withFocus={false} makes
a chart a picture — no hover dot, no keyboard cursor, no tooltip, out of the tab
order — which is what a sparkline in a table cell wants.
Stacked areas and bars now leave a 1px transparent gap between segments, since
adjacent chart colors don't meet contrast against each other. stackGap changes
or removes it. withDomain={false} lets an area fill down to zero without
dragging the value axis down to show it.
Chart tooltip's element through the instance that owns it rather than a document-wide query, so the lookup stays correct on a page with several charts if the tooltip is ever portaled out of its own container.Chart with line and bar marks, built on TanStack Charts, under new @customerio/pluma-components/charts/react and @customerio/pluma-components/charts/ember entrypoints. Series colors come from the data-viz tokens, and tooltips use Popover's content styling with React render props / Ember blocks for custom content.SplitView: pixel minSize/maxSize constraints now hold when the split changes size. The constraints resolve against the measured pane space, but a change in that space only re-rendered the split when a pixel-sized pane was present — so a pane clamped to maxSize={320} kept its percentage share and grew right past 320px when the split widened. The same stale clamp let a hidden pane come back at roughly half the split: showing a pane whose resizer mounts in the same commit can catch one measurement while the grid template lags the DOM, and the pane's minSize resolved against that garbage space inflated it — with nothing ever recomputing the layout once the next, correct measurement arrived. A change in the measured space now re-renders the split whenever any pane declares a pixel constraint, so the clamps are always resolved against the space actually on screen.@customerio/pluma-components/data-viz entry with palette utilities: sequential, semantic, categorical, diverging, defaultColor, dataVizSubset, and the pre-computed categoricalSlots/sequentialPalettes/semanticPalettes/divergingPalettes. Each returns DataVizColor descriptors ({ type, index, name, token, tokenHover }) that reference theme-contract CSS vars, never raw hex; a literal count returns a fixed-length tuple.SplitView: sub-pixel measurement noise no longer reads as a resize of the split. With a pixel-sized pane present, a fractional-pixel wobble in the measured pane space (browser zoom, fractionally-sized containers) re-rendered the split on every measurement until React gave up with error #185 ("Maximum update depth exceeded"). The measured space now only moves on a change of more than half a pixel, and smaller differences are compared against a stable anchor so they can never read as a fresh resize on every pass.--pluma-c-checkbox-checked-disabled-check-color override for the Checkbox's check mark.c2f87a2: Add ConditionViewerItem and ConditionViewerGroup for rendering a read-only summary of a set of rules — a boxed sentence per rule, nested under the operator that combines them — plus ConditionViewerTerm for the emphasized runs in a sentence and ConditionViewerDetailList/ConditionViewerDetail for the sub-conditions a rule is refined by.
The wording, marker icons and recursion stay in the application; Pluma renders the boxes, typography, spacing and semantics.
3ef12fa: Stepper test helpers: adds stepper.clickStep(label) and step.click(), so tests no longer have to query the DOM to navigate a stepper. Both find the step's control whether it renders as a link or a button. Neither is a silent no-op — they throw on an unknown label, and on a step that is disabled or non-interactive, since both render a control that drops the click.
Also fixes step.isDisabled() and step.isCurrent() returning true when the helper is scoped to a step that isn't there; they now return false.
ff7ad3b: Add PageRail, a column beside PageBody's content for a vertical Stepper, a table of contents, or a summary. Write it before or after the content to put it on that side — or one of each — and it sizes to its content and can stay in view while the sections beside it scroll. The content keeps the width its preset asks for and stays centred on the page while there's room for the rail beside it, giving way only when there isn't.
Also adds PageBodyContent, which wraps the sections a rail stands beside. It's only needed in a body that has a rail.
a67791c: Stepper: adds two attention values to a step's status — critical for a step whose errors block finishing the flow, and caution for one that needs review but doesn't block. Each marker takes its semantic color plus the icon Pluma pairs with that semantic (error and warning), and carries visually-hidden text ("has errors" / "needs attention") so neither state is conveyed by color alone. content="minimal" markers stay bare dots, so avoid it where a step's status carries weight. A step can be flagged and isCurrent at once: aria-current is unchanged and the status keeps the marker. Connector fill is unaffected.
a67791c: Stepper: the current step's ring is now an accent-colored outline around the marker rather than a border inside it, matching Timeline's current marker. It sits outside the coin, so it reads the same around a neutral, completed, or flagged marker instead of competing with that state's own color. A step that isn't current no longer gets an inert isCurrent-false class.
The ring is configured with three new CSS variables — stepper-current-coin-outline-color, stepper-current-coin-outline-width, and stepper-current-coin-outline-offset. stepper-current-coin-border-color is deprecated in favour of stepper-current-coin-outline-color but still recolors the ring, so an existing override keeps working; the same goes for the currentCoinBorderColor and currentCoinBorderWidth keys on the style exports, which now resolve to their outline equivalents.
19f903e: Adds Page design guidelines
015d008: The automatic text color applied by a filled backgroundColor now comes from that surface's text-on-* token instead of text-invert, which could not carry the right polarity for both dark fills (bold, navigation) and light ones (caution, information, the -bold variants). This fixes contrast on backgroundColor="bold" and "navigation" in light mode, and on caution, information, and every -bold surface in dark mode.
Badge, Calendar, SegmentedControl, and Stepper take their text color on filled surfaces from the matching text-on-* token for the same reason. Light mode is unchanged throughout.
Text and Heading accept a new black value for color, which comes with the new text-black token.
99a92ae: Adds Strong design, a11y, and AI guidelines
42084b7: Every remaining button now renders aria-disabled="true" instead of the native disabled attribute and blocks interaction programmatically, completing the migration started for Button and PlainButton. This covers the Select trigger, options and collapsible group headers, Tabs tabs, Calendar days, Date picker presets, dropdown menu items, the Combobox open button, the Snackbar and Banner buttons, and Breadcrumbs items. Disabled styling is unchanged.
Two things to update when upgrading: CSS and test assertions targeting :disabled / [disabled] on these elements need [aria-disabled='true'], and because aria-disabled doesn't suppress events for descendants, your own interactive content inside a disabled component now receives clicks. Native form inputs and Form's <fieldset disabled> are unchanged.
513db70: OptionCardGroup: add a cardsLayout="grid" option that lays the cards out on a real grid, configurable with the same grid props the Grid component takes (cardsColumns, cardsRows, cardsAreas, cardsAutoFlow). Also adds cardsColumnGap, cardsRowGap, cardsAlignContent and cardsJustifyItems to the card container.
Stepper: the connecting line is now green only between two steps that are both status="complete", instead of filling up to whichever step is isCurrent. Flows that let people jump ahead to a future step no longer show green lines over steps that aren't done. A step that is both complete and current now also takes the current step's ring, on top of its green marker.PageBodyContentWidth, StepperSize, TimelineColor, SnackbarLayout, Filter, SelectOptionItem and 70+ others — from both the React and Ember entry points. The Ember entry also exports every component's Pluma*Signature, matching React's *Props.PlumaCheckPlumaBoxSignature is now PlumaCheckboxSignature, PlumaSelectMenuPopoverSignature is now PlumaSelectPopoverSignature, and the drawer close button's signature is now PlumaDrawerCloseButtonSignature rather than shadowing PlumaDrawerTitleSignature. None were reachable from an entry point before, so no published name changes.CloseReason to ModalPrimitiveCloseReason, matching its sibling PopoverPrimitiveCloseReason. The old name was never reachable from an entry point, so nothing published changes.Stepper: steps no longer resize as the current step moves. The current step's label is bolder, and bolder text is wider, so a fit stepper (where every step is as wide as the widest one) shifted its whole grid when the customer stepped forward. Every step now reserves the space its label needs in that bolder state.PageSection, a band that stacks inside PageBody with its own contentWidth and an optional divider between it and the next section. Content widths gain a full preset so a single body or section can break out of a narrower page default.ff7bb7f: Extend the elementBefore / elementAfter slot pattern beyond NavItem. DropdownMenuItem, Select options, the Select trigger, TextField, Button and Link all gain the two slots, which compose with each component's built-in content rather than replacing it. In React they are node props; in Ember they can be passed as a component or, except on Select options, via the :elementBefore / :elementAfter named blocks.
Button and Link also gain iconLeading and iconTrailing, shortcuts for an icon fixed to one side so both sides can carry an icon at once. They render in addition to icon/iconPosition, never in place of it.
TextField.unsafe_leftSectionComponent, TextField.unsafe_rightSectionComponent and Select.unsafe_leftSectionComponent are deprecated in favour of elementBefore / elementAfter. They keep working until the next major.
004f5da: SplitView: two layout fixes.
A groupResizeBehavior="preserve-pixel-size" pane now keeps its pixel size when a sibling pane is hidden or shown. Its share was being renormalized along with the relative panes, so hiding a neighbor grew it — a 320px pane beside a hidden 30% pane became 457px instead of staying 320px and letting the relative panes take the freed space.
minSize and maxSize now also hold when the layout is computed, not only while dragging. A pane that declared no size takes whatever is left over, which could fall under its own minSize when its siblings' declared shares used up the split (a pane with minSize="25%" beside 320px, 30% and 30% siblings rendered at 8%). The shares are now brought inside each pane's constraints, taking the difference from the panes that have room to give and asking panes that hold an exact size (a controlled size, or preserve-pixel-size) last.
SplitView: a pixel size/defaultSize no longer makes a pane unresizable. Pixel values now seed a pane's share of the split, so the resizers beside it work like any other, and a pane that asked for a specific share gets exactly that rather than an average. To keep a pane at a literal pixel size while the split resizes, set the new groupResizeBehavior="preserve-pixel-size" on SplitViewPane.isResizable={false} no longer keeps the resize cursor and hover affordance — previously only its tab stop was dropped.pointer-events: none while dragging, so an iframe in a pane can no longer swallow the drag.979b845: Badge absorbs the capabilities of the deprecated Label component (FPS-2955).
New Badge props (purely additive — the existing contract is unchanged):
color — neutral (default), accent, neutral-outline, accent-outline, plus Label's red, raspberry, clementine, yellow, green, teal, blue, plum, purple, and grey. The label-family colors render in Label's squarer shape and are pixel-identical to Label; the neutral/accent colors render as the familiar pill.bold — defaults to true (today's weight); set false for less prominent text.showStatusIndicator — Label's status dot, with the same constraints (only red, yellow, green, blue, purple, grey; bold text only).Deprecations (everything keeps working until the next major version):
Label / PlumaLabel component — use Badge with the color prop instead (color="outline" becomes color="neutral-outline").isEmphasized prop — use color="accent" instead.isOutline prop — use color="neutral-outline" instead (color="accent-outline" for the combination with isEmphasized).The color prop cannot be combined with the deprecated isOutline/isEmphasized props.
c41b6fe: Badge gains semantic color values: critical, caution, success, and information render identically to red, yellow, green, and blue but keep the intent readable at the call site. Use them when the color conveys meaning; the raw colors remain first-class for decorative categorization. The Badge test helpers gain getSemanticColor(), which reports the semantic meaning of the rendered color regardless of which spelling was used.
5105bc3: PageBody gains a contentWidth prop to constrain the body independently of the page, and Page gains a matching bodyContentWidth prop that sets the default for every body it contains (a contentWidth on an individual PageBody wins). When neither is set, the body inherits the page width as before (standard, or full-bleed when the page is isFullScreen). "standard" forces the standard page width (min 950px, max 1850px) even under a full-screen header, and "narrow" caps the body at 800px with no minimum width.
fd16bc3: Add Stepper, a horizontal navigation component for communicating progress through the ordered steps of a wizard or multi-step flow.
05e0881: Add a vertical orientation to Stepper via a new orientation prop (horizontal default, or vertical). Vertical stacks the steps in a column with each label beside its marker — for a sidebar guide or a long form chunked into stages. States, colors, fill behavior, markup, and ARIA are identical to horizontal; only the axis changes.
ac471b5: Default Text/Paragraph no longer renders dark-on-dark inside TooltipContent or Snackbar. The default (base) text color now resolves through a --text-color-default variable that inverted surfaces override, so plain text adopts the surface's color without needing color="inherit". Link children inside a tooltip now adopt the surface color too.
resolvedVars, privateVars, and defaultVars to the affected component style-exports. Additive and behavior-preserving.SplitView resizing on touch devices. The resizer only listened for mouse events, which touch does not synthesize during a drag, so it could not be dragged with a finger. Both frameworks now drive resizing with pointer events (mouse, touch, and pen), and also end the drag on pointercancel.AspectRatio layout component for constraining content to a fixed width-to-height ratio.SplitView layout component (React + Ember) for building resizable, nestable config/preview split layouts with SplitViewPane and SplitViewResizer. Supports flexible and fixed panes, min/max sizes, hide/redistribute, snapping, resize callbacks, and full keyboard accessibility. Panes take an optional id for stable identity across conditional mounts, so each pane-set configuration keeps its own layout. Includes a useStoredSplitView hook (React + Ember) that persists a split's layout to storage (localStorage by default), with per-configuration layouts for conditional panels.useDrawers, useDrawer), mirroring the Modal manager. The Drawer component now accepts a state prop for use with the manager.withImmediateCallbacks in the modal manager's closeAll/closeAllOfType, matching close. Previously waitForClose promises for modals closed in bulk stayed pending until the close animation finished even when withImmediateCallbacks was true.markerStatusIndicatorColor to TimelineItem, passed through to the marker coin's status indicator.unsafe_isLegacy (Button, DatePicker, Link, Select, TextArea, TextField, ToggleButton) and the unsafe_useLegacy* PlumaProvider flags. Behavior is unchanged in this release — the flags still apply their overrides — and they will be removed in the next major version.onBeforeToggle and controlled isOpen to Accordion. onBeforeToggle({ isOpen }, event) runs before a toggle and can cancel it by returning (or resolving to) false; it may be async to defer the toggle. isOpen makes the accordion controlled, and onToggle now receives the triggering event as a second argument.6b96b73: Fix a Glimmer render crash (Cannot destructure property 'manager' of … undefined) when a Button, PlainButton, or ToggleButton is rendered with a component @as (a non-href @as={{SomeComponent}}).
The aria-disabled work (2.181) started currying the component @as to inject isDisabled — (component this.as isDisabled=…). When that curried component is invoked by the underlying Box (<Element ...attributes>) with the button's forwarded {{on "click"}} interaction modifiers, Glimmer can schedule a modifier/component instance whose manager is undefined, which throws during the render-transaction commit and takes down any page rendering such a button.
The non-href @as component is now rendered directly again (as it was before 2.181). Disabled state is still conveyed to it via the forwarded aria-disabled attribute, so no behavior is lost. The href branch (which curries PlumaPlainLink) is unchanged.
28e3a06: Add a size prop to Avatar (matching Coin's sm | md | lg | xl tiers, defaulting to md) and expose the initials typography via text-prefixed props: text, textSize, textWeight, textFamily, and textFontStyle. The initials text scales with size to stay balanced inside the coin (overridable via textSize/text).
Note: Avatar's size now derives entirely from the underlying Coin (default md, 32px). Previously the avatar was pinned to a fixed 28px, so avatars with no explicit size now render at 32px.
ticks, snapToTicks, snapToleranceFraction, and onTickClick props to Slider. Ticks render as marks along the track (with optional clickable labels below), and soft snap applies to the value on release only — continuous drag and keyboard nav are unaffected.bfbc150: Panel padding can now be customized per side. In addition to the existing panelPadding one-var override, Panel exposes panelPaddingBlockStart, panelPaddingBlockEnd, panelPaddingInlineStart, and panelPaddingInlineEnd CSS vars. Each side falls back to panelPadding, which falls back to the default, so panelPadding keeps working as a single-value override.
Set these vars on a panel (or a container above it) to customize the whole panel's padding, or on an individual PanelSection to override just that section's padding — the section's negative inset margins keep cancelling the panel's padding, so overriding a section no longer needs competing padding declarations and isn't sensitive to the order component stylesheets are imported in.
A Table inside a Panel now resolves its edge-cell padding from the matching per-side panel padding rather than a single value; for panels with uniform padding this is unchanged.
FileInput component (React and Ember). It supports a single-button mode and a drag-and-drop dropzone mode (dropToAdd), with loading and selected-image states. The dropzone is built on native HTML drag-and-drop and file input APIs.4106b80: Buttons now render aria-disabled="true" instead of the native disabled attribute when disabled (via isDisabled, isLoading, or a disabling Button context). This applies to Button, PlainButton, CloseButton, and ToggleButton.
A natively disabled element is removed from the accessibility tree and the tab order, so screen readers skip it and it can't be focused — which breaks tooltips and other affordances that explain why a button is disabled. Using aria-disabled keeps disabled buttons focusable and announced (as disabled), while click and keyboard activation are still blocked.
Behavior change to be aware of:
disabled attribute, and no longer set tabindex="-1" — they remain in the natural tab order. The disabled styling is unchanged.button[disabled] / :disabled in CSS, or asserting the disabled attribute in tests, should switch to [aria-disabled='true'].a789ffa: Add shouldShowOpenButton prop to Combobox for hiding the trailing dropdown-toggle button. Also fix onClear reopening the dropdown when shouldOpenOnFocus is enabled.
2bf386b: Radio, Checkbox, and Toggle now render the description prop. Previously the prop was part of their public API (inherited from the shared form-control type) but was never displayed. The description renders below the label, aligned with it, using the standard form description style.
90527b8: Add the Timeline component (Timeline / TimelineItem, and PlumaTimeline / PlumaTimelineItem in Ember) for rendering ordered sequences of events with markers and a connecting rail.
Each TimelineItem supports a markerIcon, markerIconSrc, or markerContent (text, any node, or an empty string for a plain coin); a title, description, and meta (leading or trailing via metaPosition); per-row color, trackColor (rail color independent of the marker), markerSize, shape, lineVariant, and isCurrent. Rows with no marker render full-width with the rail passing through them, for hosting a Banner or other context. Color and line variant carry across the seam to the next row, and unsafe_color / unsafe_colorAbove / unsafe_trackColor allow one-off custom rail colors. Includes Timeline and TimelineItem test helpers.
Europe/Kiev/Europe/Kyiv, America/Godthab/America/Nuuk, Asia/Rangoon/Asia/Yangon, and Asia/Saigon/Asia/Ho_Chi_Minh.unsafe_leftSectionComponent to Select, matching the leading-section prop already on TextField, Search, and NumberField. It renders custom content on the left side of the select trigger — before the selected value and after any icon — for cases the icon prop can't express, such as always-included tokens or a custom badge.position: initial on the Tabs tab element so its pseudo elements are positioned relative to the wrapper rather than the tab itself.elementBefore and elementAfter slots to NavItem for rendering custom content (e.g. an icon) in the leading and trailing parts of the item. In React these are node props; in Ember they can be passed as a component or via the :elementBefore / :elementAfter named blocks. Both compose with the built-in icon/arrow rather than replacing them: elementBefore renders after the icon, and elementAfter renders before the expand arrow when withArrow is set.e9ca77f: Avatar: added a withGravatar prop to control the email-based Gravatar lookup, and changed the default behavior so Gravatar is now off by default.
Previously, passing an email without a src always resolved a Gravatar image. Now the avatar derives initials/label from email but does not fetch a Gravatar unless you opt in with withGravatar (withGravatar={false} remains the default). This avoids surfacing third-party images for emails where that isn't appropriate (e.g. your customers' own contacts).
If you relied on email automatically showing a Gravatar, add withGravatar to those avatars:
Search (Ember): the className prop is now forwarded to the inner TextField, so it correctly applies to the component's outermost element. Previously it was dropped, matching the existing React behavior.PageHeader updates:
PageHeaderTitleAction slot. Renders inline to the right of PageHeaderTitle (after PageHeaderLabel if present) and is meant for inline action affordances that operate on the page subject (e.g. an edit button). Generic slot — pass any content, typically a single icon-only Button.PageHeaderLabel now renders to the right of PageHeaderTitle (previously to the left), matching the latest design spec. Existing consumers automatically pick up the new layout — no API change.PageBreadcrumbs. No layout change — it lives as a sibling of PageHeader inside Page, not inside the header grid, so existing pages render exactly as before. New code should render the standalone Breadcrumbs component above the page header instead. PageBreadcrumbs will be removed in the next major release.e42296b: Ember: stop overlays from throwing when their floating position is recomputed while the host is going away
Reading an overlay's floating position reads floatingUi.options, which calls
the host component's options function — and therefore the host's args and
getters. Those getters can touch state that has already gone away (e.g. a
destroyed Ember Data store, or an unloaded record), in which case they throw,
the error escapes the run loop, and it fails otherwise-unrelated work in the
host app (this surfaced widely after consumers upgraded @glimmer/component
to v2, which changed teardown ordering).
This happens via two routes:
FloatingUi#isTearingDown now also checks the owner — which, unlike the
resource or host component, is reliably flagged as destroying during that
flush.PositionFloating#position now bails with the last computed data if reading
the host options throws, instead of letting the error escape.Normal overlay open/close/positioning behaviour is unchanged.
4247bb4: Ember: stop overlays from re-evaluating their floating position during owner teardown
When a host application/engine instance is destroyed, it flags itself as
destroying and then flushes a final render queue before tearing down individual
components. That flush re-invokes the reference/floating modifiers, which read
PositionFloating#position — and reading position reads floatingUi.options,
i.e. the host component's args and getters. Those getters may already touch
torn-down state (e.g. a destroyed Ember Data store), so they throw, which fails
test teardown and cascades into unrelated tests.
The existing guard only checked the floating resource and host component, but
neither is flagged as destroying at flush time. FloatingUi#isTearingDown now
also checks the owner (application/engine instance), which is reliably flagged
as destroying during that flush, and PositionFloating#position returns the
last computed data instead of reading options while tearing down.
transitionend does not fire.icon prop to Select so React and Ember consumers can render a decorative leading icon in the trigger.ColorSchemePreference and ResolvedColorScheme from the React and Ember Pluma provider entrypoints.onRowExpansionChange callback to DataTable.xs size for Toggle.description rendering and child title/description composition components, with React and Ember codemods for migrating existing Banner children/yields into descriptions.2415d3b: Fix polymorphic type checking for as prop — JSX.IntrinsicElements → React.JSX.IntrinsicElements
The global JSX namespace doesn't exist in TS 5.x with jsx: "react-jsx". The emitted .d.ts referenced bare JSX.IntrinsicElements, which resolved to nothing in consumers, causing the conditional type to fall through and silently accept any prop when as was used (e.g. <Box as="div" garbage="hello" /> compiled without error).
$extensions.mode resolve to the referenced token's value for the same mode. The tokens build now emits Cobalt UI's internal {token#mode} reference format for generated mode aliases, matching the format Cobalt's Figma collection parser uses for mode-specific variable references, and the components theme build strips that suffix when converting original aliases back to CSS variables.DataTable showing a stray bottom border on the last visible row when virtualization is enabled and there are no more rows to scroll to. The virtualizer's end placeholder row now signals its empty state via a data-pluma-table-end-placeholder attribute, which TableTd uses to suppress the preceding row's bottom border.SegmentedControl icon shifting horizontally when the selected option changes. The icon and label now share the same fixed position across both the active (bold) and inactive (regular) states.label prop to DataTableHeaderColumnsMenu. When provided, the trigger button displays visible text alongside the icon instead of rendering as icon-only.date prop to accept null | undefined (renders — fallback). Treat 0 as missing. Auto-detect epoch seconds vs milliseconds for numeric inputs (values below 10_000_000_000 are converted to ms).4ade8ba: Button: add unsafe_withMigratedButtonVariants flag (also configurable
via PlumaProvider's componentConfig.PlumaButton). When set, button
variants render as if the upcoming major release that removes the
secondary variant has already shipped:
secondary renders with current tertiary stylestertiary renders with current subtle stylesIntended to be enabled together with running the
codemods/button-variant-rename codemod. With the flag on, the codemod
can be applied safely before the breaking-change major release — your
app looks the same before and after the rename. When the major
eventually ships, drop the flag.
This flag supersedes unsafe_withSoftDeprecatedSecondaryVariant: when
both are set (or just the new one), the secondary → tertiary mapping
is still applied.
4ade8ba: Add codemods (codemods/button-variant-rename/) that consumers can run
ahead of the upcoming Button variant renames: tertiary → secondary and
subtle → tertiary. Separate transforms ship for React (.tsx/.jsx)
and Ember (.gts/.gjs/.hbs); see the codemod README for instructions
on running it directly from this repo without publishing an npm package.
Pair this with the new unsafe_withMigratedButtonVariants flag (see
separate changeset) so the renamed source renders identically to the
pre-codemod look. When the major release that drops secondary ships,
drop the flag — your app keeps rendering the same.
c862593: Add Coin component — a shaped container (circle or rounded square) of fixed size that frames an Icon, Image, or short text content, with optional status indicator and label-token colorVariant. Coin propagates size, shape, imageFit, and color to descendants via context, so child Icon and Image components auto-size and Images can fill the coin edge-to-edge when imageFit="cover".
Deprecates LabelIcon (use <Coin><Icon /></Coin> instead) and Avatar's icon/iconSize props (compose with Coin for non-user-avatar use cases). Both will be removed in a future major version.
icon prop still suppresses the initials fallback as before.icon and iconSize props to AvatarconfirmationPhrase option to ConfirmationModal. When set, a text field appears requiring the user to type the exact phrase before the confirm button becomes enabled.SegmentedControl indicator briefly animating in from the default position on initial render. The first position update now uses an inline transition-duration: 0ms override instead of relying on the parent's :has()-driven CSS variable, whose cascade can fail to reach the indicator before style.transform is mutated.react/utils/* subpaths. The types field pointed to dist-react/react/public-utils/ but files are at dist-react/public-utils/.ed0c209: Adds Banner design an a11y docs
418c70f: Add early return with dev warning when icon name is undefined
76ab318: fix: replace raw palette refs with semantic tokens for dark mode compatibility
Components were referencing palette primitives directly instead of semantic color tokens, which breaks on dark themes since palette values don't change between modes.
palette['grey-100'] → color['border-minimal']palette['grey-100'] → color['surface-subtle']palette['grey-100'] → color['surface-subtle']palette['grey-100'] → color['surface-subtle']dataTableSelectedRowBackgroundColor and dataTableNestedSelectedRowBackgroundColor component vars with current palette values as fallbacksicon-white color token, and fix up tokens in Button and Snackbarvalue is an empty string.text-wrap: pretty to heading components to improve text wrapping and avoid orphaned words on the last line.3eacb12: Drawer: Add lg, xl, and xxl sizes and subtitle prop
5b419db: Remove legacy pseudo-element fade styles from Scrollable
Removes the fadeLeft, fadeRight, fadeTop, and fadeBottom pseudo-element
classes from the Scrollable component. These used sticky ::before/::after
elements with white-to-transparent gradients which didn't work well on
non-white backgrounds.
The withEdgeIndicators prop (using mask-image) is now the only approach.
The Select multi-value component has been migrated to use maskFadeRight.
color prop passing and adjust sizingwithEdgeIndicators pseudo-element approach with CSS mask-image classes. This fixes edge fade indicators for horizontal flex layouts and simplifies the implementation. The fade size can be overridden via the scrollable.vars.fadeSize CSS variable.a99dbc4: Add onLoadMore and onSearch support to DataTableHeaderColumnsMenu
3ca22c5: Expose state prop to ModalPrimitive
secondary button variantstyle to be passed in as an arg to TextFieldradius-4 to radius-6.KeyboardShortcut component that displays keyboard shortcuts with platform-aware formatting, powered by @tanstack/react-hotkeys. Supports isInverted prop for dark variant.withArrow prop to Tooltip component. When set to false, the arrow indicator on the tooltip content is hidden. Defaults to true to preserve existing behavior.9631f8c: fix(DataTable): stabilize config objects to prevent infinite re-renders
Add use*Config hook wrappers (usePaginationConfig, useSortingConfig, useSearchConfig, useFiltersConfig, useEmptyStateConfig) alongside the existing get*Config functions. The hooks memoize the result using the actual prop dependencies, keeping the plain functions intact for ember usage.
The get*Config functions return new objects every render. When used directly as contextValue useMemo dependencies, every render produced a new context, forcing DataTableTable to re-render infinitely.
102419f: fix(DataTable): fix infinite re-render loop in DataTableTable
table.getFlatHeaders() returns a new array reference on every render. Using it directly as a useLayoutEffect dependency caused setColumnSizing() to fire every render, which triggered a TanStack Table state update, causing another render — resulting in an infinite loop. This was triggered when consumers passed unstable prop references (e.g. inline filters or bulkActions objects).
Fixed by deriving a stable string key from header IDs to use as the effect dependency instead of the array reference.
6d7d6d0: fix(TextArea): pass onKeyDown and rows props to inner TextAreaInput
The React TextArea component accepted onKeyDown and rows in its type definition but silently dropped them — they were never forwarded to the inner TextAreaInput. This caused onKeyDown handlers to never fire and rows (without minRows) to have no effect.
isClearable to Select componentJsonField input*px format, when specifying widths in the data tableisAlwaysShown option to filters9cb19c7: Ensure the unix timestamp input adheres to any minDate/maxDate validations in the Datepicker when using withTime
Adjust PlumaModal test helper dialog getter to use queryByRole instead of getByRole to prevent test timeouts when checking modal state
onConfirm and onCancel callbacks to the ConfirmationModal6042268: DatePicker component withTime prop
withTime prop: Enable time selection in DatePicker componentreference prop: Allows for DST labels to be calculated correctly based on a reference date (either before or after DST)withTime={true}, component returns ZonedDateTime instances instead of CalendarDatetime-zone.ts for managing time selection stateSELECTABLE_TIME_ZONES and getSelectableTimeZones() from date-picker utilswithTime cannot be used together with isRange (mutually exclusive as seen in Figma)isActive stateisOpen and close function to the PopoverPrimitiveisActive prop to Button componentlabel prop to the Divider componentPopoverTriggerButton componentedgeThreshold and overflow=hidden option to Scrollable componentwithEdgeIndicators option to the Scrollable componentAvatar from shrinking in flexbox layoutssize option to LabelIconv2 sizing for icons, moving on from fly sizingisDraggable option to popover and popover primitive to make them draggableminRows and maxRows to the <TextArea /> componentisActive prop to <Blanket /> componentdirection prop to the <Divider /> componentname through inputstype="button" to Tabs when rendered as button elementsdescription option to the Select component<DropdownMenuFooter /> componentisPulsing to the StatusIndicator componentText props in the ember Truncate componentStatusIndicator componentScrollable componentisExternalTruncated componentisLoading and autoLoading to banner actionnull as a value for the TextField icon proppromotion variant to EmptyStatewithPadding property to panel sectionFilters componentsize and isInline props to the Image componentCloseButton componentalign meta option for data table configurationisDisabled prop to Tag componentDescription component to PanelProgressCircle componentvariant support for banner actions and added isOutlined prop for bannersPlainButton componentsize option to option cardtooltip prop to the DropdownMenuItemshouldCloseOnClick to DropdownMenu<Image /> componentdescription option to DropdownMenuItem componentminDate, maxDate, isDateDisabled to DatePicker componentonClick to breadcrumb itemsisLoading prop to <Page::Body /> component<PopoverPrimitive::Item/> component for creating navigable items inside popoversYield internal utility for react0 to autoCloseTimeout in the snackbar should prevent it from closing automatically<Banner /> to cautionerror and success variants to the snackbarstatusIndicatorColor option to <Avatar />shouldSubmitOnEnter to the TextArea component<Form /> componentwithOverlay option to disable overlays for drawersshouldAnimate option to disable animation<Avatar /> componentshape prop to <LabelIcon /> component<Drawer /> component<PlumaModalPrimitive /> componentautofocus attribute to TextField componenttruncate property to text componentsisError and replaced it with isDanger for the <Button /> component<FormLayout /> component<SkeletonShape /> component<SkeletonText /> componentsize option to <Modal /> componentonClick property for link component<ProgressBar /> component<Divider /> component<Select /> component when mixing keyboard and mouse navigationpageHeaderTitleText class<Select /> component<Select /> component<Search /> componentisLoading prop to <PlumaPage /><PlumaPage/> component
<PlumaPageHeader /> to <PlumaPage::Header /><PlumaPageHeader::Title /> to <PlumaPage::Header::Title /><PlumaPageHeader::Description /> to <PlumaPage::Header::Description /><PlumaPageHeader::Actions /> to <PlumaPage::Header::Actions /><PlumaPageHeader::Breadcrumbs /> to <PlumaPage::Breadcrumbs />onPageSizeChange and pageSizes options to the Pagination component. Together these render a Select component to switch the size of the pages the user paginates through.52f49d1: Update tokens and components to match post-brand-refresh Figma state.
This change introduces a few breaking changes. It modifies existing styles and changes the available props or variants on some components.
index.js