Skip to content

ExpressiveCSS v0.16.0

Latest

Choose a tag to compare

@BaezFJ BaezFJ released this 28 Oct 03:21
· 8 commits to master since this release
f2e0f02

This release removes the aliases kept for markup written before 0.8.0. See
Removed and Migration below before upgrading.

Deprecated

  • Chip sizing properties --padding, --font-size and --font-size-icon.
    Use --md-comp-chip-padding, --md-comp-chip-label-size and
    --md-comp-chip-icon-size instead. Each new property takes priority over
    its old counterpart. Old overrides on a chip remain supported as fallbacks,
    defaults are unchanged, and there is no removal date.

Added

  • Native multiple-leaf TreeSelect through <select data-tree multiple>, with
    independent leaf toggles, an open popup and optional search retaining hidden
    choices. Native form submission includes all selected leaves and no branches.
    Branch defaults never propagate into leaf choices; reset and refresh retain
    native ownership. Selection and focus remain separate on the named tree.

  • Opt-in single-leaf TreeSelect search through searchable: true. Matching leaf
    paths open without changing the native selection. Clearing or closing restores
    pre-query expansion, including through refresh and disabled-control recovery.
    Search reuses accessible popup controls, localization and lifecycle handling
    while retaining tree keyboard behavior and native leaf submission.

  • Independent drop-zone file-count limits through data-max-files. Exact-count
    choices pass; too many files invalidate the complete retained selection with
    accessible count feedback and expressive:reject reason count. All selected
    files appear in independent rejection snapshots. Shared Forms handling keeps
    type/size precedence, application validity, deduplication and reset recovery.

  • Optional per-file drop-zone byte limits through data-max-size, independent
    of type checking. Exact-limit files pass; one oversized file invalidates the
    whole retained selection with accessible feedback and expressive:reject
    reason size. Combined rules preserve type-first feedback and application
    custom validity. Constraint-aware deduplication, reset and shadow handling
    reuse the existing Forms path.

  • Opt-in drop-zone type validation with data-validate-types, native form
    prevention and accessible error feedback. Extensions, exact MIME types and
    standard media wildcards match whole selections without filtering files.
    Browse and drop share validation; expressive:reject reports invalid choices.
    Recovery preserves application validity and supports open shadow roots.

  • File reset cleanup now follows native form ownership, including external
    associated inputs, and runs after reset dispatch/default handling. Canceled
    resets preserve current file state; newer selections retain their feedback.

  • Server-owned data table paging through silent validated DataTables.setPage
    state and cancelable expressive:page requests. Server responses retain
    every supplied record without local slicing, filtering or sorting. Local
    page and size controls now request before applying their default; cancel
    to preserve the accepted view. Null state restores local paging.

  • Authored single-select page-size controls for local data tables. Changing
    size preserves an available page or clamps to the new last page, retaining
    selections and matching counts.

  • Local data table paging through a fixed data-page-size and authored
    Previous/Next buttons. Paging follows filtering and sorting, keeps details
    with records, scopes select-all to the displayed page and preserves
    selections on other pages. Silent sync clamps the current page after row
    changes. Ordinary pagination links retain their navigation behavior.

  • Server-owned data table filtering through cancelable expressive:filter
    requests with a trimmed, case-preserving query. Cancel synchronously to
    preserve the current view, then replace records and call
    Expressive.DataTables.sync(wrapper). Sync stays silent and preserves the
    application's server count; an uncanceled input resumes local filtering.

  • Opt-in data table text filtering. A labelled search input with
    data-table-filter searches columns whose headers have data-filter.
    Record details stay together, author-hidden rows remain hidden, and an
    authored status output can announce the result count. Select-all now
    affects only enabled records visible in the current view; hidden records
    retain their selections. To include hidden records in a bulk operation,
    check them from script and call Expressive.DataTables.sync(wrapper);
    expressive:change continues reporting all selected records.

  • Opt-in select filtering with FormSelect's searchable option. A
    separate popup search field filters grouped options while the native select
    retains its form value. Includes localized search and no-results text,
    keyboard navigation, query clearing on close and reset, and query-preserving
    refresh. On a multiple select, picks keep the popup open with the query and
    focus in place, and options the query hides stay selected and submitted.
    Default, tree and browser-default selects keep their behavior.

  • Autocomplete option groups. An entry's group lists it under a labelled
    heading, and entries with the same group gather where the group first
    appears. Up and Down pass over headings, the announced result count leaves
    them out, and a click on one chooses nothing. Entries without a group keep
    their behavior.

  • Disabled Autocomplete entries. An entry with disabled: true stays in the
    results, dimmed, and a click, Enter and selectOption() leave it
    unselected, or selected when the selected option or
    setMenuItems(items, selected) chose it. Up and Down skip it, and the
    announced result count leaves it out. Entries without the flag keep their
    behavior.

  • Independent gap utilities. .gap-x-0 through .gap-x-6 set column gaps and
    .gap-y-0 through .gap-y-6 set row gaps on the existing rem spacing scale.
    Both support all four window-size prefixes and override the gap shorthand
    at the same breakpoint. Existing gap utilities retain their behavior.

  • Responsive child sizing. .flex-1, .flex-none and .shrink-0 now take
    window-size prefixes. New .flex-initial and .shrink-1 classes restore
    content sizing and shrinking, with the same four prefixes.

  • Responsive self-alignment. .self-start, .self-center, .self-end and
    .self-stretch take the medium:, expanded:, large: and
    extra-large: prefixes, and the new .self-auto returns a child to its
    parent's align-items. self-center expanded:self-auto centers a child
    until Expanded and then follows the parent. The unprefixed classes are
    unchanged.

  • Responsive visual order. .order-first, .order-last and the new
    .order-none (order: 0) take the medium:, expanded:, large: and
    extra-large: prefixes. order-last expanded:order-none moves a child to
    the end until Expanded and then back to its source position. The classes
    change visual order only; Tab and reading order still follow the source.

  • Show more. A .show-more holds text with a line-clamp-1, -2 or
    -3 class and a <button>; the button shows the full text, reads
    "Show less" while it is open (i18n.showLess or data-show-less) and
    sets aria-expanded and aria-controls. The button hides while the
    clamped text fits and comes back when a resize or a web font cuts it.
    New semantics rules require a <button type="button"> and clamped text.

  • Treemap chart. A <figure class="treemap-chart"> draws each row of its
    table's first value column as a rectangle whose area is its share of the
    total, filling the plot in a squarified layout from the largest value
    down, mirrored in right-to-left pages. Each rectangle shows its name and
    cell when they fit, and rows with no positive value draw nothing. The plot
    is one Tab stop whose arrow keys step through the rectangles in table
    order, the tooltip adds each row's share, and it takes update() and
    data-watch like the other charts. A new semantics rule asks for a
    <figure> with a <table> and a <figcaption>.

  • Hover card. A link with hover-card-trigger names a .popover with
    popover="hint" in aria-details, and once the pointer or keyboard
    focus has rested on the link for 500 ms, that card shows against it.
    The card stays while the pointer or focus is on the link or the card,
    and hides 300 ms after both are left, or at once on Escape with focus
    back on the link. A tap or click follows the link and shows no card.
    Two new semantics rules keep the trigger an <a href> whose
    aria-details names a [popover].

  • Transfer list. A .transfer-list holds a .transfer-available and a
    .transfer-chosen list of checkbox rows with data-transfer buttons
    between them. add and remove move the checked rows, add-all and
    remove-all every row, and a row moved back returns to its original
    place. The form submits the chosen rows' values under the container's
    data-name, in the chosen list's order, and a form reset puts the rows
    back. Each move fires expressive:transfer with { rows, from, to }
    and is announced through TransferList.i18n.moved. Four new semantics
    rules ask for data-name, unnamed checkboxes, named lists and
    type="button" on the move buttons.

  • Funnel chart. A <figure class="funnel-chart"> draws each table row as
    a stage, from the top down in table order, centered and as wide as its
    value against the largest one. The tooltip adds each stage's percentage
    of the first row, and further columns show in the tooltip. The plot is
    one Tab stop whose up and down arrows move between rows, and it takes
    update(), where stages move from their old widths, and data-watch
    like the other charts. A new semantics rule asks for a <figure> with a
    <table> and a <figcaption>.

  • Sankey chart. A <figure class="sankey-chart"> draws each table row as
    a flow: the first column names the source node, the second the target
    node, and the third holds the value. Nodes stand in columns by their
    longest path from a source and stack in the order they first appear.
    Each link is as thick as its value, and each node as tall as the larger
    of its in and out totals. Rows without a positive value or both names,
    and rows that would close a cycle, draw nothing. The plot is one Tab
    stop whose arrow keys step through the links in table order, and it
    takes update() and data-watch like the other charts. A new
    semantics rule asks for a <figure> with a <table> and a
    <figcaption>.

  • Loading overlay. While the page sets aria-busy="true" on a
    .loading-overlay, a surface scrim covers it, its loading indicator
    shows in the centre, and the script makes the region inert, so
    nothing inside takes a click, a tap or keyboard focus. A visually
    hidden status region after it announces the start and the finish, in
    words set with i18n, and focus goes back to the control that had it
    when the region clears. A new semantics rule asks for a direct
    .loading-indicator child.

  • Dialogs.confirm() asks a yes-or-no question in a basic alert dialog
    and returns a promise that resolves true on the confirm button and
    false on Cancel, Escape or a scrim tap. Focus starts on Cancel and
    returns to the opener, and the dialog is removed once it closes.
    Dialogs.i18n holds the default labels, "OK" and "Cancel".

  • .back-to-top is an in-page link that appears once the window has
    scrolled past threshold, one viewport height by default. Activating it
    scrolls to the top, instantly under reduced motion, and focuses the
    element its href names. The BackToTop component keeps it inert
    while hidden and watches the threshold with an IntersectionObserver.
    Without the script the link stays visible and jumps to its target. A
    focused link stays until focus leaves, and modified clicks are left to
    the browser.

  • .defer-render makes the browser skip layout and paint for a long
    list's off-screen rows through content-visibility: auto. Skipped rows
    reserve --defer-render-row-size, the list item height by default, and
    focus rings inside a deferred row are drawn inset so the row does not
    clip them. Rows with swipe actions are left alone, and table rows are
    not supported.

  • Line clamp utilities. .line-clamp-1, .line-clamp-2 and
    .line-clamp-3 cut text to that many lines and end the last one in an
    ellipsis, and .line-clamp-none shows every line again. All four take
    the medium:, expanded:, large: and extra-large: prefixes.

  • Pointer helpers. .pointer-events-none lets clicks, taps and hovers
    pass through an element to what lies beneath it, and
    .pointer-events-auto gives them back to a child, such as a button in
    a caption overlay. .cursor-pointer shows the pointer cursor and
    changes nothing else. Neither pointer-events class affects focus or
    keys; use disabled or inert to block all input.

  • .opacity-0, .opacity-25, .opacity-38, .opacity-50, .opacity-75
    and .opacity-100 set an element's opacity. 38 is the M3 opacity for
    disabled content.

  • .list-unstyled removes a <ul> or <ol>'s markers and inline-start
    padding and keeps its margins. A new semantics rule,
    list-unstyled-has-list-role, flags one with no role, because WebKit
    drops the list role from a list without markers. Add role="list", or
    the role the list should have.

  • Position and inset helpers: .position-absolute, .position-fixed and
    .position-static, .inset-0, and the edge pins .top-0, .bottom-0,
    .start-0 and .end-0. The start and end pins use logical properties, so
    they swap sides in right-to-left pages. The classes take a position-
    prefix because the navigation bar, toolbar and table of contents already
    use .fixed as a modifier. They have no window-size prefixes, and
    .position-fixed sets no z-index.

  • .grid-auto-fill lays out as many columns as fit, each at least
    --grid-column-min wide (15rem by default). Set the property on the grid or
    an ancestor. A container narrower than the minimum gets one column, and the
    last row keeps the column width. Pair it with .gap-*.

  • Data table column resize. A <button class="drag-handle"> ending a
    header cell lets people resize its column by dragging, with the arrow
    keys (16px, 64px with Shift), Home and End, or a click, Enter or Space
    that steps through narrow, middle and wide. Each change fires
    expressive:resize from the wrapper with { header, column, width },
    and DataTables.setWidth() restores saved widths without firing it. The
    new --md-comp-data-table-column-min-width token sets the narrowest
    width.

  • Data table column visibility. A menu checkbox item with data-column
    set to a first-row header cell's id hides that column's header and
    cells while it is unchecked, and an item unchecked at load starts its
    column hidden. Cells that span several columns stay, and resized columns
    keep their widths. DataTables.sync() applies the toggles for markup
    rendered later. The framework stores nothing; save the choice from the
    menu's expressive:change.

  • .sticky-column on a .data-table keeps the first column in view while
    the other columns scroll sideways, in both writing directions. When the
    first column holds row checkboxes, the name cell after it stays pinned
    too, and the checkbox column takes the width of the new
    --md-comp-data-table-checkbox-column-width token. Pinned cells keep
    their row's hover and selected colors, and a divider marks their edge.

  • Calendar week, day and year views. table.calendar.week and
    table.calendar.day are hourly grids: a row per hour named by a
    <th scope="row">, and a column per day whose header can carry
    aria-current="date". An event sits in the cell for its day and
    starting hour and covers one hour, or --md-comp-calendar-event-duration
    hours, starting --md-comp-calendar-event-start hours into the row.
    The placement is only visual, so write each event's time in a
    .visually-hidden span. Events that overlap in time overlap on screen,
    and an event past the last hour row spills out of the table. .calendar-year lays
    out twelve month tables in as many columns as fit. The new
    --md-comp-calendar-hour-height token sets the row height (48px).

  • AutoInit(context, options, { watch: true }) keeps watching the
    context after the first scan. Components start in markup added later,
    with the same options, markup options and no-autoinit opt-out, and
    are destroyed when their markup is removed. Elements that already have
    an instance keep it, moved elements keep theirs, and shadow roots are
    watched only when passed as the context. The call returns a function
    that stops watching. Without watch, AutoInit() behaves as before.

  • Timepicker's minTime and maxTime options bound the times a person
    can choose, as 24-hour HH:MM or HH:MM:SS. Dial ticks outside the
    range are dimmed and refused, the fields stop stepping at the bounds and
    refuse typed times outside them, and a value outside the range opens at
    the nearest bound. On a 12-hour clock, a period with no allowed time has
    its AM or PM button disabled. Ranges can't cross midnight. Without the
    options every time stays open.

  • Timepicker's showSeconds option adds a seconds field after the
    minutes. It is a spin button named by the new i18n.seconds, it steps
    and takes typed values like the minute field, and the picker writes
    HH:MM:SS, with AM or PM on a 12-hour clock. The dial keeps its hours
    and minutes views. A type="time" input needs step="1" to hold the
    seconds without a step mismatch. The default of false keeps HH:MM.

  • AutoInit() reads options from markup. An element's
    data-<name>-options attribute holds a JSON object of that component's
    options, with the name in kebab case: data-menu-options,
    data-form-select-options. Options passed to AutoInit() win for the
    keys they set. Values must match the kind of their default, so callbacks,
    elements and null-default options stay script-only, as do
    allowUnsafeHTML and the Chips and Autocomplete data lists. Invalid
    values log a warning and keep their defaults. Direct init() calls do not
    read the attribute.

  • Tooltip's openOnClick option opens the tooltip on a click instead of
    hover and focus. A second click, Escape, a click elsewhere or focus moving
    to another element closes it, and Enter or Space on a button trigger
    toggles it. On touch a tap is the click, so the long press is off. The
    default of false keeps hover and focus.

  • A chart's update() moves its marks from the old values to the new ones.
    Line chart lines, areas and points, column chart columns, bar chart bars
    and mixed chart marks move over --md-sys-motion-duration-medium4 with
    --md-sys-motion-easing-standard. A series or value new to the chart
    rises from its base, and one that goes away goes at once. With reduced
    motion the new shape appears at once. Migration: after update(),
    columns and bars no longer grow again from zero, and lines no longer jump
    to their new shape. No page change is needed.

  • Autocomplete's allowCreate option ends the list in an Add "text" row
    when the typed text matches no suggestion exactly, ignoring case and
    surrounding spaces. Choosing it with Enter or a click calls onCreate with
    the trimmed text and fires expressive:create with { text }. The
    selection does not change; the page adds the entry. i18n.create sets the
    row's label. Chips add the text as a chip, in an autocompleteOnly field
    too.

  • When a search returns no suggestions, Autocomplete's open list shows
    i18n.noResults as its first row, so sighted users see what the live
    region announces. The row is li.autocomplete-no-results with
    role="none" and aria-hidden="true". Arrow keys skip it, and a click on
    it chooses nothing and keeps focus in the field. It leaves the list when a
    search returns a match. Arrow keys and Enter now move through
    li[role="option"] rows only. In Chips, Enter that adds the typed text
    as a chip now closes the list.

  • A chart with data-watch on its figure, or the watch option, redraws
    itself when its table changes, so page script no longer calls update().
    It watches added and removed rows and cells, cell text, and data-value,
    class and the heatmap's data-label inside the table, and it follows a
    replaced table. The edits a script makes in one run redraw it once. Every
    chart type takes it, and destroy() stops it. Charts without it behave as
    before.

  • Autocomplete's searchDelay option waits that many milliseconds after
    typing pauses before calling onSearch, so a remote search runs once per
    pause instead of once per key. A text change aborts the running search's
    signal right away. A search on focus runs at once, and blur, destroy()
    or choosing a single result cancels a pending one. The default of 0
    keeps the search on every input. Chips take it through
    autocompleteOptions.

  • A menu item with role="menuitemradio" checks itself and unchecks the
    other radio items in its group. A group is a <ul role="group"> with an
    aria-label inside an <li role="none">; radio items outside a group form
    one group per menu. Rows in a group are styled and reached by arrow keys
    and type-ahead like any other row. A click or Enter checks the item and
    closes the menu while closeOnClick is on. Space now checks it and keeps
    the menu open, where it used to close the menu. A change fires
    expressive:change from the trigger with { item, checked: true }, after
    onItemClick and expressive:select; choosing the checked item fires no
    change. Page code that synced aria-checked on radio items can go. It
    writes the same values, so leaving it in place does no harm.

  • A menu item with role="menuitemcheckbox" toggles its own
    aria-checked. A click or Space flips it and keeps the menu open, so
    several items can be toggled in a row; Enter flips it and closes the menu
    while closeOnClick is on. Each toggle fires expressive:change from the
    trigger with { item, checked }, after onItemClick and
    expressive:select. Migration: a page that flipped aria-checked itself
    in onItemClick or a click listener must remove that code, or the two
    flips cancel out.

  • A .validate field sets aria-invalid="true" along with invalid, and
    clears both once the value passes, so a screen reader hears the error
    state the color shows. Its [data-error] supporting text joins the
    input's aria-describedby, keeping the ids already there, so the error
    message is read with the field. An empty data-length no longer marks a
    field invalid.

  • Tooltips show on a long press on touch, as Android does: after half a
    second held, without enterDelay, staying for a second and a half after the finger lifts. A
    tap used to show the tooltip through its compatibility mouse events and
    leave it up until the next tap elsewhere; a tap now shows nothing, and a
    press that turns into a scroll shows nothing. The click a long press's
    release sends is consumed, so it does not press the button or follow the
    link.

  • Menus take placement: 'top' to open above the trigger when they fit
    there, as for a trigger in a bottom bar, and Home and End jump to the
    first and last rows. An anchored menu that does not fit below its trigger
    now opens above it when there is more room there. Its height was capped
    to the room below, so it shrank and scrolled instead of flipping.

  • Chips submit a form value. Give the field's <input> a name and the
    chips submit under it, one id per chip, as a multiple select does.
    init wraps the input in <expressive-chips-control>, the framework's
    first custom element. It is form-associated, has display: contents and
    takes the name, so half-typed text is not submitted. The browser handles
    the field's reset, validity and disabled state: a reset puts back the
    starting chips within the reset, and a disabled fieldset drops the values
    at once. Setting disabled, readonly or form on the input applies at
    the next microtask. A required input makes the field valid once it has a
    chip, and says the new i18n.required until then, with
    aria-required="true" on the input. That validity is on the control,
    apart from any error the page sets on the input. A read-only input's field
    is not required. destroy() unwraps the input and gives its name and
    required back. Without form-associated custom elements, as in jsdom, the
    input keeps its name and required and submits its text. Pasting text
    that lists values, split by commas, tabs or new lines, adds a chip for
    each, up to limit.

  • Data tables sort and select. With data-sort on a .data-table, a
    button in a header cell reorders the rows by its column and moves
    aria-sort. A numeric column compares numbers, other columns compare
    text in the page's language, data-value stands in for a cell's text, and
    a detail row moves with its row. A cancelable expressive:sort lets a
    server-paged table sort the rows itself. A checkbox in the first header
    cell now selects every enabled row and shows mixed for some, and each
    change fires expressive:change with the selected rows.
    Expressive.DataTables.sync() updates the select-all box after rows are
    checked from script. Pages that sort with their own script and no
    data-sort are unaffected.

  • Charts with a series legend take .toggle. Each legend item becomes a
    button with aria-pressed that shows or hides its series, and the scale
    refits to the series left. The other series keep their colors, the last
    one shown with data cannot be hidden, and a hidden series stays hidden
    through update(). Line, column, bar, mixed and radar charts have it.
    Without the class the legend stays decorative.

  • The Auto Init page documents Alpine.js. A short x-expressive directive
    starts a component each time Alpine renders its element, including inside
    x-if and x-for, and destroys it when Alpine removes the element. The
    page also shows how to keep a Select in sync with x-model by calling
    refresh(). A browser test runs the directive against Alpine in Chromium,
    Firefox and WebKit.

  • Charts have update(), which redraws a chart from its changed table in
    place. Before, the way to redraw was to call init() again, which dropped
    keyboard focus and the highlighted row. Now focus stays on the chart, the
    highlighted row stays where the table still has it, and a scrolled heatmap
    stays scrolled. A chart set up with an empty table draws once it has rows.

  • Tabs can be a tablist. A strip the page writes as role="tablist", with
    role="tab" links naming their panel in aria-controls, gets the ARIA
    tabs pattern: Tabs keeps aria-selected and a roving tabindex, so the
    strip is one Tab stop, and Left, Right, Home and End select as they move.
    Link tabs with aria-current stay the default. semantics.json allows the
    roles only when the markup is complete: named, on a <div>, every tab a
    role="tab" with aria-controls. In either mode an overflowing bar
    scrolls the selected tab into view, without scrolling the page, and with
    no .active tab or matching hash the first enabled tab is selected rather
    than a disabled first one.

  • Scatter chart. A <figure class="scatter-chart"> plots each table row
    as one point per series: the second column holds the x value, and each
    further column is a series of y values. data-x-min and data-x-max, or
    the xMin and xMax options, fix the x scale, and axis labels both
    scales. The plot is one Tab stop whose arrow keys step through the points
    in table order, and it takes update(), data-watch and a toggle
    legend like the other charts. A new semantics rule asks for a <figure>
    with a <table> and a <figcaption>.

  • Bubble sizes on the scatter chart. With bubble, the last column sizes
    each row's points, so their areas follow its values, and the largest is
    --md-comp-scatter-chart-bubble-size (48px) across. A row without a
    positive size draws no points. The tooltip names the size, a toggle
    legend refits the sizes, and bubbles grow or shrink from their old sizes
    after update(). Room around the plot keeps a bubble at the end of a
    scale clear of the axis labels and legend.

  • Pull-to-refresh. A touch pull down from the top of a .pull-to-refresh
    container, or its <button data-refresh>, fires expressive:refresh and
    shows the container's loading indicator with aria-busy="true" until
    every promise passed to event.detail.waitUntil() settles or the page
    calls complete(). The threshold option sets the pull distance (64px).
    A visually hidden status region announces the start and the finish, in
    words set with i18n. The container and the page holding it get
    overscroll-behavior-y: contain, so the browser's own pull-to-refresh
    stops. New semantics rules
    ask for the button and a direct .loading-indicator child.

  • Swipe actions. In a .list.swipeable, a touch swipe toward a row's
    inline start slides it and reveals its .swipe-actions buttons. A swipe
    only reveals; nothing runs until a person presses an action. The row's
    <button data-swipe-toggle> opens and closes it without a swipe, focus
    on an action opens its row, and Escape, a tap outside or an action closes
    it. One row is open at a time. Each change fires expressive:swipetoggle
    with { row, open } and runs onToggle; open(row) and close() work
    from script. A new semantics rule asks for the toggle in every row with
    actions.

  • Input mask. A text field with data-mask formats what people type,
    paste or compose to the pattern: 0 takes a digit, a a letter, *
    either, and other characters are literals the mask writes, such as
    (000) 000-0000. The caret stays after the edit, Backspace and Delete
    remove the digit across a literal, an IME composition is formatted once
    it ends, and saved and reset values are formatted too. The form submits
    the value as shown. A new semantics rule keeps data-mask on input types
    that have a caret.

  • Currency input. A text field with data-currency="EUR" shows its
    amount in the page's locale and that currency once it loses focus, such
    as 1.234,50 € under lang="de", and the plain number 1234,5 while
    it has focus. The last . or , is the decimal mark when no more than
    the currency's fraction digits follow it, so 1234.5 and 1.234,5 read
    the same. The form data holds the plain number, 1234.5. Text with no
    digit stays as typed and marks the field invalid, with a message i18n
    can replace. The instance's value returns the number. A new semantics
    rule keeps data-currency on text inputs.

  • Cascader. A .field.cascader with a named text input and nested
    ul.cascader-options lists shows the options as columns in a popover,
    where each choice opens the next column. Arrow keys, Home, End, Enter,
    Space, Escape and Tab move through the columns, swapped under rtl.
    Choosing a leaf shows the path in the input and fires input and
    change, and the form gets one entry per level, such as place=us,
    place=ca, place=sf, through a form-associated control. data-selected
    on a leaf sets the starting path, which a reset brings back, and
    required blocks submit until a path is chosen. New semantics rules keep
    the source lists plain and every option valued.

  • Tree select. A <select data-tree> whose options name their parent's
    value in data-parent opens an ARIA tree, where the arrows, Home, End,
    * and typeahead move between visible rows and only a leaf can be
    chosen. The native select keeps the form value, reset and validity, and
    branch options are disabled while it runs, so the form never submits
    one. refresh() keeps open branches and focus. AutoInit starts
    TreeSelect on select[data-tree] and FormSelect on other selects, and
    a new semantics rule keeps data-tree on single selects nested only
    through data-parent.

  • A data-draggable map marker moves without a drag (WCAG 2.5.7). A tap
    picks it up and sets aria-pressed, and the next tap on the map drops it
    there. Escape or a second tap puts it back, and the arrow keys nudge a
    focused one. A marker that is not a <button> gets role="button" and
    tabindex="0" while the map runs.

  • Map controls take pan-up, pan-down, pan-left and pan-right actions,
    which move the view a third of the map's size, so it pans without a drag.

  • Menu, Lightbox, NavigationRail, Cards and ExpandingCard fire
    expressive:open and expressive:close DOM events from the element
    they were initialized on. Menu, Lightbox and NavigationRail then fire
    expressive:opened and expressive:closed. The events bubble and cross
    shadow roots, and each follows its on… callback, which still works.

  • Choices and changes fire events too, with what was chosen in detail:
    expressive:change from Tabs ({ panel }) and Carousel
    ({ item, dragged }), expressive:select from Menu items ({ item }),
    Datepicker ({ date }) and Timepicker ({ hours, minutes }),
    expressive:confirm and expressive:cancel from both pickers,
    expressive:autocomplete ({ values }), and expressive:chip-add,
    chip-select and chip-delete ({ chip }). A callback that changes the
    choice again leaves only the event for the final one. Tabs now calls
    onShow after hiding the old panel, and Chips callbacks get the instance
    as this, as every other component's do.

  • The rest report too: Tooltip fires expressive:open and
    expressive:close when it shows and hides; Snackbar fires open,
    action, close and closed; ButtonGroup fires change with
    { button, pressed }; ScrollSpy fires change from the section with
    { link }; Tour fires open, change with { step, index } and close
    with { completed }; and copy buttons fire expressive:copy with
    { text, copied }. Tour.end(el, completed) takes the flag from script.

  • Sheets spring back with the finger's speed: a bottom or side sheet
    released short of dismissing solves its spring at run time from where
    it is and how fast it moves, and a press while it springs catches it in
    place. Each motion spring gains -damping and -stiffness tokens.

  • Arabic, Hebrew and Thai text: Noto Sans Arabic, Noto Sans Hebrew and
    Noto Sans Thai ship at 400, 500 and 700, each limited to its script's
    unicode-range so a page fetches one only when it shows that script,
    and --md-ref-typeface-fallback lists them before sans-serif.

  • Autocomplete onSearch gets an AbortSignal as its third argument. It
    aborts when a newer search starts or the autocomplete is destroyed, so a
    page that passes it to fetch() drops stale responses.

  • Linear progress .stop puts a dot in the indicator colour at the
    track's end, and .wavy draws the indicator as a wave over a thin track,
    on <progress>, .determinate and bare bars, and a circular indicator
    takes .wavy too: a wave round the ring over a thin track. New tokens:
    --md-comp-progress-stop-size and --md-comp-progress-wavelength.

  • Common button shapes: square takes the size's squared corner, every
    common button morphs to its size's pressed corner while pressed, and
    aria-pressed makes a button a toggle that swaps round and square when
    selected and takes the M3 selected colours for its style.

  • Questionnaire extras, each opt-in on the form: data-shortcuts lets a
    letter or digit pick an answer, data-summary adds a review step that
    lists every answer with an Edit button before submitting, and
    data-save="key" keeps the answers and the shown question in
    localStorage until the form is submitted.

  • Keypad and keyboard keys: data-key="CapsLock" holds capitals until
    pressed again, and data-key="Layer" shows the elements whose
    data-layer matches its value, such as a row of symbols. A hardware
    key held down in the field presses the matching on-screen key. The
    keypad now checks minlength on its own edits, with the
    i18n.tooShort message.

  • Radial chart fills: data-segments splits each ring into segments filled
    whole, and .gradient shades each fill along its ring, from a light
    tint at the start to its full color where the fill ends.

  • Range columns and bars: a series header with the class range reads
    cells such as 120–480, 3 to 14 or data-value="120 480" and floats a
    column or bar between the two ends, rounded at both.

  • Mixed chart stacking: .stacked stacks the column series into one
    column per row, with the end scale's columns in their own column
    beside it. Lines and areas keep their own values.

  • Line and mixed chart .points marks every value on a line with a dot,
    and .straight draws straight lines between values instead of a curve.

  • Heatmap scales: data-mid makes the scale diverge from that value, with
    values below it shading towards the new
    --md-comp-heatmap-chart-low-color (tertiary), and data-steps groups
    the shades into bands, with a swatch per band under the grid.

  • Wide heatmaps scroll: cells keep --md-comp-heatmap-chart-cell-min-width
    (12px), and a grid too wide for its figure scrolls its cells sideways
    instead of narrowing them, while the row labels stay in view. The arrow
    keys scroll the cell they move to into view.

  • Pie chart grouping: data-other="5" turns every row under 5% of the
    whole into one last slice with their sum and share, named by
    data-other-label ("Other" by default). The table keeps every row.

  • Radial chart names: .labels draws the rings three quarters of the way
    round and writes each row's name in the empty top left quarter, beside
    its ring's start. Names shrink to fit crowded rings. On a gauge, the names
    hang under the base line, a row per ring with the innermost on top, each
    after a line down from its ring's start. A sparkline ignores it.

  • Pie chart names: .labels writes each slice's row name outside the pie,
    beside its slice. Names that would touch move apart along their side, a
    line leads from each name back to its slice, and the pie shrinks to leave --md-comp-pie-chart-label-width (96px), or a
    quarter of a narrow figure, on each side.

  • Expandable bottom sheets: dialog.bottom-sheet.expandable opens at
    --md-comp-bottom-sheet-partial-height (50% of the viewport) and
    .expanded grows it to the full height. A drag up expands it, a drag
    down collapses it before it dismisses, and a handle <button> switches
    between the heights with aria-expanded kept current.

  • Arrow keys for tabs and lists: Left, Right, Home and End move focus
    between tabs, wrapping and skipping disabled ones, and Up, Down, Home and
    End move it between the rows of a .list whose rows are each one link or
    button. Tabs and rows stay links and Tab stops; focus moves without
    choosing. The list keys come from the new Lists behavior.

  • Modal date and time pickers: displayPlugin: 'modal' opens the picker in
    a modal <dialog class="display-modal"> drawn as a basic dialog, named by
    the input's label. Ok writes the input; Cancel, Escape and a press on the
    scrim close it without a change and call onCancel. Focus moves into the
    dialog and back to the input. Selective Sass builds add "dialog" and
    "modal-display".

  • Value axes for charts: .axis on a line, column, bar or mixed chart
    labels its value scale at round values, with a grid line at each, and
    widens an end the chart chose to the next round value. The labels repeat
    the cells' currency or percent sign and shorten large numbers. A mixed
    chart labels its end scale on the far side and stretches it so both
    label sets share the grid lines. On a radar chart, .axis rings the grid
    at round values and labels the rings.

  • Non-Latin text faces: Roboto and Noto Sans now ship Latin Extended,
    Cyrillic, Cyrillic Extended, Greek, Greek Extended and Vietnamese subsets,
    and Noto Sans adds Devanagari, at 400, 500 and 700. Each face has a
    unicode-range, so a browser downloads only the scripts a page renders.

  • Cut corners: --md-sys-shape-corner-family (round or bevel), a
    corners="cut" switch that cuts the corners of cards, dialogs, sheets,
    menus, fieldsets, buttons, chips, alerts, snackbars, tooltips, popovers and
    outlined fields, and a .corner-cut utility. Built on CSS corner-shape;
    browsers without it keep rounded corners.

  • Spring motion tokens from Material 3 Expressive:
    --md-sys-motion-spring-{fast,default,slow}-{spatial,effects} with a
    -duration token each, solved at compile time into linear() easings.
    motion="expressive" on the root or a subtree selects the expressive
    spatial springs. The Sass spring($damping, $stiffness) and
    spring-duration() functions build custom ones.

  • Emphasized type roles from Material 3 Expressive: .{role}-emphasized
    classes for all fifteen roles and
    --md-sys-typescale-emphasized-{role}-font-weight tokens, at 500 for
    display, headline, title-large and body and 700 for title-medium,
    title-small and label. Roboto and Noto Sans now ship a 700 face, fetched
    only when a page renders that weight.

  • A "Design tokens" documentation page at /tokens.html lists every token
    family, its prefix and the page that documents it, with an override
    example.

  • Material 3 motion tokens: --md-sys-motion-easing-* for the standard,
    emphasized and linear curves and --md-sys-motion-duration-short1 to
    extra-long4. Components now read them wherever they used an on-scale
    curve or time, so :root { --md-sys-motion-duration-short4: 150ms; }
    speeds up every 200ms transition. Computed values are unchanged.

  • Material 3 corner tokens: --md-sys-shape-corner-none to full, with the
    Expressive large-increased, extra-large-increased and
    extra-extra-large steps. Component shape tokens on the scale read them,
    so :root { --md-sys-shape-corner-medium: 8px; } squares off every medium
    corner. Computed values are unchanged.

  • Elevation tokens: --md-sys-elevation-level0 to level5 and
    level1-half, generated from the $elevations map. The z-depth()
    mixin, the .z-depth-* classes and every component read them, so a theme
    can change a shadow level at runtime. Computed values are unchanged.

  • --expressive-z-* properties name the stacking order of fixed and floating
    surfaces, from toolbar (996) to confetti (10001). Components read them
    instead of literal z-index values, and the Shadow page lists them.

  • Layout helpers take a window-size prefix: .medium:, .expanded:,
    .large: and .extra-large: versions of the display, flex direction and
    wrap, justify-*, align-*, text alignment, .gap-*, margin and padding
    classes, and .hide, apply from that size class up, so
    flex flex-column expanded:flex-row stacks until Expanded.

  • More helpers: .block, .inline-block, .grid, .inline-grid,
    .contents, .relative, .sticky-top, .flex-row, .flex-nowrap,
    .justify-around, .justify-evenly, .align-baseline, .self-*,
    .order-first, .order-last, .shrink-0, .w-full, .h-full,
    .max-w-full, .aspect-1x1, .aspect-4x3, .aspect-16x9,
    .object-cover, .object-contain, .bordered, .border-top,
    .border-bottom, .border-start, .border-end, .corner-none to
    .corner-full, .text-balance and .text-pretty.

  • Print styles. Paper uses the light scheme and hides navigation rails,
    navigation bars, the FAB menu, snackbars and tooltips. External links in running text print their address, headings
    stay with what follows, and table headers repeat on each page.
    .hide-on-print and .print-only cover the rest.

  • .prose styles a container of bare HTML, such as rendered Markdown: a
    70-character measure, heading and list spacing, underlined links in lists
    and headings, and a surface for inline code and <pre>. It only touches
    elements without a class and sits in the base layer, so components inside
    it keep their own look.

  • .query-container makes an element a query container. Horizontal cards
    and description lists inside one stack when it is narrower than 600px, so
    they adapt to a sidebar or pane on a wide window.

  • Contrast levels: contrast="medium" and contrast="high" on the root or
    a subtree select M3's medium and high contrast schemes. Accents, their
    containers, on-surface-variant and outlines move to tones that reach
    about 4.5:1 and 7:1 against the surface. prefers-contrast: more applies
    the high scheme unless a contrast attribute is set, and
    contrast="standard" opts out.

  • Color swatches component: a fieldset.color-swatches of radios in
    labels with visually hidden names, each drawn as a round chip of the color
    set in --md-comp-color-swatch-color, with a ring around the checked one.
    An <input type="color"> at the end adds a custom value. CSS only.

  • Number stepper: two trailing .icon-button.suffix buttons with
    data-step="down" and data-step="up" in a .field step its
    <input type="number"> within min, max and step, and fire input
    and change. The browser's spin buttons are hidden. The Forms behavior
    handles it, so there is nothing to initialize.

  • Color picker component: ColorPicker enhances a .color-picker around an
    <input type="color"> with a saturation and brightness area, the
    framework's slider as a hue control, and a hex field. The native input stays
    the form value and fires input and change. The area is a keyboard
    slider with a spoken value. AutoInit starts it.

  • A "Frosted glass" documentation page at /glass.html covers the
    --expressive-glass, -blur and -opacity properties, the Sass defaults,
    the surfaces that frost, and the reduced-transparency fallback, with live
    demos. A button in the docs app bar turns glass on across the site.

  • A navigation rail destination can open a .navigation-rail-panel side
    panel of more links. Make the destination a <button popovertarget> and
    put the panel popover right after it. The panel docks beside the rail at
    full height, a click toggles it, and with a mouse hovering the destination
    opens it. <details> groups inside the panel are its dropdowns.

  • .org-chart draws a reporting line top down, with each person as a card
    under their manager. Write nested lists; a manager's card is the
    <summary> of a <details> that folds the team away. Cards take an
    .org-chart-role band, an .avatar and an .org-chart-name. A wide
    chart scrolls sideways. CSS only.

  • The expressivecss-migration agent skill moves a project from Materialize
    or an older 0.x release to current names in eight steps: package and
    assets, JavaScript API, removed components, classes and markup, the aliases
    0.16.0 removes, silent visual changes, and verification. Each step ends with a
    search for the names it replaces, and the skill installs on its own with
    npx skills add BaezFJ/ExpressiveCSS.

  • An "AI agents" documentation page at /ai.html lists the plain-text docs,
    the install command for the agent skills, the MCP server configuration and
    the expressivecss-lint command. Its skills table is read from each
    skills/<name>/SKILL.md, so a new skill appears without editing the page.

  • Menubar component: a .menubar of .menu-trigger buttons gets the menubar
    and menuitem roles, one Tab stop, Left, Right, Home and End along the bar,
    and Left and Right between open menus. Each item opens an ordinary Menu.
    AutoInit starts it.

  • Mega menu component: a nav.mega-menu of links and buttons whose
    .mega-menu-panel popovers open wide panels of grouped links below the bar.
    CSS only, on native popovers.

  • Tour component: a .tour manual popover walks through <section> steps
    beside each data-tour-target, with Back, Next, Done, progress text,
    skipped missing targets, Escape and focus return. Expressive.Tour adds
    start(), go() and end().

  • Image compare component: a .image-compare reveals its second image up to
    a native range input's value. The ImageCompare behavior moves the
    divider, and Slider no longer initializes that range.

  • Calendar component: a page-rendered table.calendar draws a month grid
    with weekday headers, today, outside days and event chips. CSS only.

  • Sortable lists: a .sortable list reorders by dragging an item's
    .drag-handle or with its [data-move] buttons, announces each move and
    fires change with { item, from, to }. Semantics require the buttons.

  • <input type="color"> is drawn as a round swatch of its color.

  • Description list component: dl.description-list lays out label and value
    pairs in two columns with a divider per pair, stacking each label above its
    value at Compact widths. CSS only.

  • Error summary: an .error-summary alert in a novalidate form stops an
    invalid submit, lists one link per invalid field with its label and the
    browser's message, and takes focus. Each link focuses its field. Part of
    the Forms behavior.

  • Resizable panes: a <button class="drag-handle"> between the two panes of
    a list-detail or supporting layout resizes them by drag, arrow keys, Home
    and End, or a click that steps through narrow, middle and wide. The
    PaneResize behavior writes --md-comp-pane-list-width or
    --md-comp-pane-supporting-width.

  • Data table bulk actions and row expansion: a .data-table-actions bar
    joins the table's top edge and takes the selected color while rows are
    selected, and a tr.data-table-detail shows while the row before it holds
    a button with aria-expanded="true". CSS only; the page flips the state.

  • Alert component: .alert is a persistent message in the page flow with an
    optional leading icon, a heading and text, an .alert-actions row and a
    close button. critical switches to the error container colors. CSS only.
    It replaces the banner as the answer for persistent, nonblocking messages,
    and the banner migration page now points to it.

  • <meter> gets an 8px bar on a secondary-container track. The fill is
    primary in the optimum region, tertiary one region away and error two
    away, from the native low, high and optimum attributes. No class is
    needed, so existing meters change appearance and fill their container's
    width; set width on a meter to keep it inline.

  • Copy buttons: data-copy-for="id" copies a field's value or an element's
    text, and data-copy="text" copies the text itself. A snackbar confirms the
    result. Expressive.Copy.i18n and data-copy-message / data-copy-error
    replace its texts.

  • Theme switch: data-theme-set="light|dark|auto" on any control writes the
    theme attribute, saves the choice to localStorage and marks the current
    control with aria-pressed or aria-checked. Expressive.Theme adds
    set(), get() and sync(). Restoring a saved choice before first paint
    stays with the page's inline script.

Fixed

  • Nonsearch TreeSelect opening now honors Menu autoFocus: false and preserves
    focus already moved to a tree row before its opening animation completes.

  • Searchable tree rows inherit the shared menu tokens, preserving visible
    keyboard focus, selected colors and row spacing in light and dark themes.

  • Search popups resize after filtering so translated no-results feedback and
    newly exposed tree paths fit, including inside transformed containers.

  • Search popups stay hidden for controls initialized while disabled. Enabling
    the control and calling refresh() restores normal interaction.

  • Searchable selects preserve focus on a result reached before the opening
    animation finishes, so Enter still chooses that result in dialogs and shadow roots.

  • Escape dismisses an anchored menu while its opening animation still leaves
    focus on the trigger, including a searchable select reopened from the keyboard.
    A closed menu leaves Escape available to its surrounding dialog or page.

  • Navbar form inputs and card media captions use their complete body-large
    and title-medium type roles, including letter-spacing. Existing captions
    now use title-medium tracking, which can change their text width or wrapping.

  • The Timepicker canvas fade reads --md-sys-motion-duration-short3, changing
    its default from 175ms to 150ms. Expanded card content reads the new
    --md-comp-expanding-card-content-delay property, backed by
    --md-sys-motion-duration-short4, changing its default delay from 180ms to
    200ms. Both follow token overrides and keep their reduced-motion resets.
    Timepicker's JavaScript timers and duration option are unchanged.

  • The map popup's shadow reads --md-sys-elevation-level2, so a theme
    that changes that token changes the popup too. The shadow was written
    out in full, so the popup kept the default level 2 shadow while menus
    and cards followed the theme. It still overrides MapLibre's own shadow.

  • Chips grow with the browser's font size. Labels read the label-large
    font-size token, and the icon, minimum height, avatar, close-button box
    and editable input use rem sizes. At a 32px root, the default label is
    28px and minimum height is 64px. Default rendering at a 16px root,
    px spacing and existing custom-property overrides remain unchanged.

  • Icons grow with the browser's font size in dialogs, snackbars, alerts,
    lists, trees, timelines, the command palette, message avatars, the map
    popup, fieldset legends, the drop zone and the dialog, side sheet and
    pane header actions. Their sizes were in px, so the text beside
    them grew and the icons didn't. Icon boxes in the same rules grow with
    them, and the timeline text and the map popup text clear the larger
    badge and close button. A short timeline event stays tall enough that
    its badge doesn't overlap the next one. The Timepicker's tick text grows too; its dial
    and ticks stay in px because the script places ticks in px. At the
    default 16px size nothing moves.

  • A single-line list row sits in the middle of the row. The row grid's
    empty second line stretched, so the text sat 4px above its leading icon
    in most rows. A checkbox or radio label row also took the control's
    12px row gap and sat 7px high. In a row taller than its text, the
    headline and its supporting text now sit together in the middle instead
    of being pushed apart. A checked checkbox in a label row shows its
    checkmark on the box, not 16px before it.

  • A table cell with the hidden attribute is hidden. The global td, th
    rule set display: table-cell, which overrode the browser's
    [hidden] rule.

  • Security: Autocomplete with allowUnsafeHTML renders only a script-supplied
    text as HTML. A selected id that has no data entry, which
    data-autocomplete-options can set, used to become HTML too, so sanitized
    user markup that kept the component's class and options attribute could
    run script once the menu showed no results.

  • Security: a questionnaire with data-save no longer stores password or
    hidden inputs. It used to keep a password in localStorage as plain text
    until submit, and to restore both into the next visitor's form, which
    replaced the server's hidden values. Saved progress from an earlier
    version no longer matches and starts fresh.

  • A docked date or time picker, and a tooltip placed without CSS anchor
    positioning, stay beside their trigger on a page scrolled sideways and
    inside the body's margin. They used to be kept inside the body's box,
    which scrolls with the page, so a picker that fit the viewport could jump
    to its right edge. They are now kept inside the viewport only, and a
    docked picker with an align other than left, center or right is
    kept in view like a left-aligned one.

  • @use "expressive" with ($text-color: …) colors the page text. The
    variable was written on html, where the body rule's own on-surface
    color covered it, so it only took effect inside a shadow root. body
    and :host now take their color from $text-color. The default is
    unchanged.

  • MCP static inspection checks the require-idref and
    require-owned-descendant rules from semantics.json. The server kept
    its own copy of the rule helpers, which had drifted and skipped both, so
    markup that passed before can now get findings. It now imports the same
    rule module the repo's semantics checks use.

  • Timepicker reads AM or PM wherever it appears in a value, so
    02:30:45 PM opens at 2:30 PM. The period was looked for only after the
    first colon, so such a value, or one like 2 PM, opened as AM.

  • With a query typed, the first Escape in the command palette clears it and
    the next closes the palette, in every engine. Chromium and Firefox already
    did this for a search input; WebKit closed at once, and so did every
    engine for a text input. An Escape that cancels an IME composition is left
    to the IME.

  • In forced colors, a filled keypad key such as Call has a 3px edge. The
    fill that set it apart is dropped there, so it looked like every digit.

  • In forced colors, a determinate circular progress draws its track as a
    thin line through the ring, so the value and the track differ in shape.
    Both were full-width rings, told apart only by GrayText against
    CanvasText, which Night sky sets close together.

  • A narrow heatmap shows whole column labels. Like the line, column and
    mixed charts, it keeps every second label (or every third, and so on)
    when each is wider than its column, instead of cutting all of them to
    "W…". Labels the author already spaced out with empty data-label
    cells, such as a year of weeks named once a month, are left as written.
    The labels are now measured where they sit, so the columns' gaps and
    right-to-left grids are counted.

  • Pie slices 5 and 6 stand out from the surface. They took
    primary-container and tertiary-container, which are 1.3:1 on the
    surface in light mode and 2:1 in dark, under the 3:1 a slice needs. Each is
    now its container role mixed 45% with on-surface: the same hue, at 5:1
    in light and 6.8:1 in dark. .values text on them is surface. A page
    that sets --md-comp-pie-chart-color-5 or -6 keeps its own colors.

  • In forced colors, an invalid checkbox has a dashed edge. Forced colors
    drop the error color, so it looked like any other box. A checked invalid
    box used to paint the author's error red and on-error mark, because the
    error rules outranked the forced-colors ones; it now takes Highlight and
    HighlightText like any checked box, and a disabled one stays GrayText.

  • Chart cards in the docs keep their contents off the border, and the
    period switch sits beside the stat instead of on a row of its own. The
    column chart page now says a card needs p-4 around its stat and chart,
    as the line chart page already did.

  • A narrow line, column or mixed chart shows whole row labels. When each
    label is wider than its column, the chart keeps every second label (or
    every third, and so on) and hides the rest, rather than cutting all of
    them to "J…". Twelve months on a 320px phone show as six. The chart
    re-checks as it resizes, and the full text stays in the tooltip and the
    table.

  • A top app bar whose destinations do not fit beside the title wraps them
    onto a second row. Before, the title shrank to one letter and an
    ellipsis. On one row the bar is unchanged.

  • In WebKit, the keyboard can scroll a docs code sample that is wider than
    its block. Chromium and Firefox already put such a block in the Tab
    order; WebKit skipped it.

  • The Carousel page shows the Show all action it recommends, opening a
    full-screen dialog of the same items. It is the single-pointer way
    through a carousel (WCAG 2.5.7), and no demo had it.

  • The Enter that confirms an IME candidate no longer acts on a field. In
    the date picker it moved focus into the calendar and read the unfinished
    text; the time picker, the autocomplete's active option and the color
    picker's hex field also took it. The date picker, time picker and
    autocomplete now skip every key of a composition, so the arrows and
    Escape used to pick a candidate leave them alone too.
    Utils.isComposing(e) checks isComposing and Safari's keyCode 229, and
    Chips, Command palette, Menu and Questionnaire now share it, so Menu and
    Questionnaire also skip Safari's confirming Enter.

  • A keyboard-focused slider handle takes the shared 3px focus ring. Its only
    cue was a 10% tint around the handle, about 1.1:1 against the surface.

  • In a browser without CSS anchor positioning, a popover is centered in the
    viewport. It used to land in the top-left corner, far from its button.

  • A scrolling marquee no longer restyles every item on each frame. The
    reset's box-sizing: inherit tied each item's style to its moving group,
    so on a page that also drew every frame, each frame restyled every item
    as well as the groups. Items now set box-sizing: border-box themselves.

  • A dot map marker takes taps in a 48dp circle around its 16dp dot. It was
    a 16px target on a map that is itself a target, under WCAG 2.5.8's 24px
    minimum.

  • A snackbar with displayLength: Infinity gets its close button by
    default, as one with an action does. Without an action it could otherwise
    only be swiped away. An explicit dismissible: false is kept.

  • A nowrap breadcrumb scrolls sideways as documented. Its crumbs used to
    shrink below their text, so each separator sat on the last letter of the
    crumb before it.

  • A list row's trailing span.badge or span.meta stays in the last
    column. The label rule matched it and outranked the trailing rule, so a
    bare-text label like "Inbox" was pushed after the badge.

  • Controls grow with their text. Type is set in rem, but buttons, icon
    buttons, FABs, badges, avatars, keypad keys, text fields, selects, the
    app bar, the search bar and the slider's inset icon used px boxes. With
    a larger browser font size or 200% text-only zoom, their labels and icons
    spilled out or were cut, and a field's floating label sat on its value.
    Those sizes are now rem (a new rem() Sass function), so pages render
    the same at the default 16px. A field's clearance for its prefix, suffix,
    password toggle and number stepper grows with them.

  • Charts, cards, stats, KPIs and the org chart grow with their text too:

    • the chart height, y-axis width, bar and heatmap row heights, pie label
      room, radar label room and tooltip width;
    • the small, medium and large card heights;
    • the stat tile minimum;
    • the KPI icon box;
    • the org chart card width.
      At 200% text, axis labels no longer shrink to "8…" or "$…", a pie or
      radial total stays inside its ring, and stat tiles wrap instead of
      breaking "$48.2k".
  • A trigger with both a Tooltip and a Menu keeps both anchors. Each wrote
    the whole anchor-name property, so the second replaced the first, and
    destroying either removed the other's. Tour targets share the same list.

  • Timepicker on an <input type="time"> writes 24-hour HH:mm. A
    12-hour picker wrote 02:15 PM, which a time input rejects by emptying
    itself.

  • Tab closes an open Lightbox. The image zooms in place rather than in a
    modal, so Tab used to move focus to the page behind the overlay.

  • The data table page's sort script compares numeric columns as numbers
    and reads data-value first, so $1,200.00 sorts after $87.00.

  • Forced colors (Windows contrast themes) paint every fill as Canvas, and a
    few controls drew nothing else:

    • Filled, tonal and elevated buttons, filled and tonal icon buttons,
      keypad keys, the keyboard, the questionnaire's footer buttons, the bar
      chart track and the search bar now get an outline.
    • A pressed toggle takes the Highlight fill.
    • The select caret and check, the submenu chevron and the bottom sheet
      handle, all glyphs masked from a fill, paint in CanvasText.
    • Slider stops come back, and the inactive track is CanvasText, so
      GrayText now means only disabled.
    • Disabled checkboxes, radios and their labels turn GrayText.
    • FABs (.extra, .extend, .large.circle) and .button get the
      outline too, the column chart track is outlined like the bar chart's,
      the color picker area gets an edge, and every aura variant, rainbow and
      glow included, is the solid Highlight ring its docs describe.
  • Blockquotes, table cells and the stacked .responsive-table header use
    logical properties, so the quote bar, cell alignment and header border
    mirror in a right-to-left page.

  • The lightbox opens above a non-modal bottom or side sheet. Its overlay and
    zoomed image were at z-index: 1000, under the sheets' 1100; they now use
    --expressive-z-lightbox (1200).

  • Swipeable tabs no longer jump back to the previous panel when you pick a
    new one while the last scroll is still running. The track released its
    target as soon as its position rounded to the new panel, so the rest of
    the old scroll selected the panel it was heading to and fired onShow
    for it. The target now holds until the track arrives.

  • Static chip close buttons work again after AutoInit(). The fallback
    handler stepped aside for any .chips with an instance, and an instance
    without allowUserInput never handled the click. Chips also copy their
    initial data, ignore Enter during IME composition, and mirror arrow keys
    in right-to-left text.

  • Menu clears its old highlight when it closes or moves, so reopening no
    longer leaves two rows active. A .menu-trigger whose target id does not
    resolve now warns and stays inert instead of inserting the text "null".
    Menubar arrow keys skip disabled items.

  • A multi-select's checkbox stays checked when the box itself is clicked, and
    a disabled option in a single select no longer closes the menu.

  • Autocomplete keeps the active row when a multi-select option is toggled and
    doesn't search again. It returns focus to the field after a single-select
    click, and takes Escape only when it closes the list, so an enclosing dialog
    stays open. Results set before the field is focused don't open the list.
    destroy() removes the combobox attributes it added.

  • Datepicker sorts multiple dates the same way in every browser, puts the tab
    stop on today when nothing is selected, keeps focus on the year or month
    select across a redraw, and no longer changes the caller's minDate and
    maxDate. A typed date in multiple-selection mode joins the selection.
    destroy() removes the date input's painted value.

  • Tabs find panels whose ids contain non-ASCII characters or sit in a shadow
    root, and a shown panel keeps its CSS display instead of becoming
    block.

  • Carousel leaves Alt, Ctrl, Meta and Shift shortcuts alone, as well as arrow
    keys aimed at sliders, menus, tab lists and media controls inside a slide.

  • A snackbar swipe made within one millisecond no longer counts as a flick.

  • A textarea hidden at load (in a closed dialog or an inactive tab) gets its
    height when it is first focused or typed in instead of staying 0px tall. A
    .file-field without a path input no longer throws.

  • Tour skips steps whose target has been removed. A side sheet starts a drag
    only from its own header. Escape during a sortable drag no longer closes
    the dialog around the list.

  • Lightbox returns focus to the image when it closes and doesn't open an
    image it can't measure. Particles uses the latest visibility entry,
    repaints still dots after a theme change, and follows reduced motion as it
    changes. MapView skips data-lng-lat values with an empty part and
    handles cluster zooms and fullscreen requests that finish after
    destroy().

  • A keypad initialized before it is in the document works once inserted. The
    slider's value label follows its host when the host resizes.

  • RichTextEditor logs invalid toolbar data-attributes JSON instead of
    throwing. A required editor blocks submit while empty and takes focus,
    where the hidden textarea used to block it with no message.

  • The character counter shows a prefilled value's count and invalid state
    without waiting for focus. ExpandingCard calls onClose when its dialog is
    closed natively. Card reveals and the app bar's search view work inside a
    shadow root. ExpandingCard, AppBar, the FAB menu and CharacterCounter undo
    their ARIA and tabindex changes on destroy(), and an <a> FAB trigger
    without href is focusable.

  • A navigation rail <button> destination is now as tall as a link
    destination instead of the default button's 40px.

  • A side panel destination that comes first in a navigation rail is laid out
    as a destination, and NavigationRail no longer treats it as the menu
    toggle.

  • The navigation rail side panel keeps its 12px padding. The global
    [popover] reset in the menu styles had cleared it.

  • A modal or compact NavigationRail no longer collapses when blank space
    inside it is tapped. Its scrim is the rail's own ::before, so those taps
    looked like scrim taps; only a tap beside the rail collapses it now.

  • Touch drags on a bottom sheet's .drag-handle and a side sheet's
    <header> now dismiss the sheet. Without touch-action the browser took
    the drag as a pan and cancelled the pointer, so the sheet snapped back. The
    bottom sheet's built-in handle band and the side sheet's inner edge have no
    element of their own to carry touch-action, so on touch screens the
    explicit handle and the header are the drag areas.

  • A docked Datepicker for a date range now opens from its end input, and a
    click on that input no longer closes it. The plugin counted the end input
    as outside.

  • Menus listen for outside clicks on the document instead of <body>, so a
    click below the content of a page shorter than the window closes them.

  • A MapView marker or popup whose data-lng-lat is not "longitude,latitude",
    such as Google's latitude-first order, is skipped with a warning. MapLibre
    threw on it after the map was created, which leaked the map and stopped
    the rest of that init() call.

  • Restarting a running Tour releases the step it was showing, so only one
    target carries the highlight.

  • Autocomplete results that arrive after the user left the field update the
    list without opening it under the input.

  • A date typed into a Datepicker that the calendar would not offer, outside
    minDate/maxDate, a disabled weekend or refused by disableDayFn, now
    puts the input back to the selected date. A date before minDate used to
    become minDate in the picker while the input, and the submitted form
    value, kept the typed text.

  • A Datepicker input's initial value is now the selection, as it already was
    once the input was clicked. instance.date was undefined until then, and a
    multiple selection started empty.

  • A form reset now resets a Datepicker: the inputs added for extra dates in
    multiple selection are removed, and the selection is read back from the
    reset values. A refused date typed into one of those inputs restores that
    input's own date. A Slider's fill and value label also follow a form reset.

  • Skipping a Questionnaire question clears its error. A skipped question
    whose answer had been wrong still showed the error when the user went back.

  • A tooltip opened by hover still closes on Escape but no longer takes the key
    from the focused control, so the same Escape also closes the dialog or menu
    the user is in. On its own trigger, Escape stays the tooltip's.

  • A hover menu no longer moves focus into itself when the pointer passes its
    trigger, which pulled focus out of the field being typed in. Space, Enter or
    an arrow key on the trigger moves focus into the open menu, and Escape
    closes it from wherever focus is.

  • Clicking inside an open Autocomplete input, for example to move the caret,
    no longer closes its list. A trigger's own click is no longer treated as an
    outside click by Menu.

  • Enter in a Timepicker hour or minute field writes the typed time, after an
    emptied field takes its default as it would on leaving. With the
    default autoSubmit there is no Ok button, so a keyboard user could type a
    time but never set it.

  • Chips selection follows focus. Arrow keys used to move from the highlighted
    chip while Backspace deleted the focused one, and the highlight stayed after
    focus left the field.

  • Behaviors that listen on the document now work inside an open shadow root.
    Copy, Theme, Dialogs light dismiss, bottom and side sheet dragging, the
    bottom sheet handle button, ImageCompare, PaneResize, Sortable, Tour
    controls, the password toggle, error summary links and drop zones read the
    event's composedPath() instead of its target, which the document sees as
    the shadow host. Sortable keeps focus on a moved item's button there, Theme
    marks the controls in the clicked control's shadow root, and a sheet closed
    there in the middle of a drag reopens without the drag offset.
    Menubar moves between open menus, ScrollSpy marks links, and a card reveal
    trigger named by aria-labelledby is accepted in a shadow root too. A
    tooltip under a positioned shadow host now sits next to its trigger instead
    of offset by the host's position. change, submit, reset and toggle
    events do not leave a shadow root, so file lists, field validation, the
    error summary and Tour's own toggle handling still need light DOM.

  • Focused read-only text fields now show the focus underline. The field's
    focus styles skipped [readonly], and FormSelect makes its combobox input
    read-only, so a focused select had no visible focus indicator.

  • @use "expressive" with ($md-source: …) now sets the theme seed, as the
    changelog for 0.13.0 and the migration skill describe. The variable moved
    to abstracts/_variables.scss; before, Sass rejected the configuration
    because the tokens layer is loaded with meta.load-css().

  • The time picker's clock face is sized from dialRadius. It was a fixed
    260px while the hand and numbers centred on the default 135px radius, so
    the hand sat 5px off centre, further with a custom dialRadius.

  • Menus, context menus and select dropdowns open next to their trigger in
    right-to-left pages. The menu kept the browser's [popover] inset: 0,
    so with its explicit width the right edge won over left, and the menu
    opened against the right side of its container.

  • Unlayered CSS can set a shadow on any component again. Thirteen component
    rules that remove a shadow (text buttons, disabled buttons, docked
    toolbars, filled and outlined cards, full-screen dialogs, separated panes,
    the search view's bar) used box-shadow: none !important, and a layered
    !important beats every unlayered rule. Only the .z-depth-0 utility
    keeps the flag. A focused disabled a.button with an href no longer
    shows the enabled fill, a disabled FAB takes the disabled fill instead of
    the primary container, and an elevated pane in a separated or floating
    layout now shows its elevation. Disabled FAB menu items and toolbar FABs,
    including aria-disabled="true" ones, paint as disabled and no longer lift
    or tint on focus; aria-disabled ones still take hover, so a tooltip can
    say why. .hoverable no longer lifts a disabled element.

  • A shadow root that adopts the sheet as its only stylesheet now inherits the
    page defaults: border-box sizing, the body-medium type role and the
    on-surface text color. These were set only on html and body, which a
    shadow tree cannot reach, so its elements were content-box with the
    browser's default text color.

  • With reduced motion requested, the floating label, switch handle, icon
    button shape, slider value label, chips, time picker dial and the open card
    reveal panel no longer animate.

  • A carousel that reaches the end of its track keeps the item you moved to.
    Items past the end share that scroll position, and a one-pixel rounding
    difference in WebKit could select a later one in a right-to-left carousel.

  • Selection and checked states stay visible with forced colors (Windows High
    Contrast). Tabs, navigation bar and rail destinations, pagination, toolbar
    actions, menu and list items, mega menu links and filter chips mark the
    selected item with a Highlight outline. Checkboxes, radios and switches draw
    their checked state in system colors and get an outline on keyboard focus.
    The datepicker marks today, the selected day and the range. Small badges,
    the loading indicator and carousel dots keep their shape. A keyboard-focused
    tab also gets the focus ring instead of only a faint tint.

  • Right-to-left pages lay out text fields, checkboxes and more on the correct
    side. The floating label, prefix and suffix icons, trailing icon button,
    character counter and the select caret's inset follow the reading
    direction, as do the checkbox mark, the date range band, the modal
    navigation rail, the snackbar container and its padding, the dialog header
    form, menu item text and the nested menu's entry slide, and the map popup's
    close button. A field's label, icons and icon clearance follow the field's
    dir, so an input with dir="auto" or its own dir keeps its text clear
    of the icon. Fields and the character counter read dir; a page that sets
    only the CSS direction property renders them left to right. Known issue:
    the date picker's isRTL option reverses the cells in script, so the range
    band points the wrong way with it on, and on an RTL page it turns the
    calendar back to left to right. On an RTL page, leave isRTL off.

  • The modular entry now exports the option types of CommandPalette,
    Confetti, Particles, Keypad, MessageScroller, RichTextEditor and
    MapView, as it already did for the other components.

  • The docs now cover the styled native <meter> on the progress page, the
    onOpen and onClose callbacks of cards and expanding cards, the
    timepicker's dialRadius, outerRadius, innerRadius and tickRadius
    options, PaneResize.width() and PaneResize.setWidth(),
    Copy.textFor(), and the .overline helper.

  • A JavaScript tooltip whose trigger sits in a shadow root now starts hidden
    and absolutely positioned. The tooltip goes into that shadow root, where
    neither body > .tooltip nor :not(body) > .tooltip matched it, so it
    showed in the page flow before it was ever opened. :host > .tooltip now
    gets the same rule as body > .tooltip.

  • A tap on a menu item after scrolling the menu by touch closes the menu
    again. The scroll flag was cleared only by a click, and a touch scroll
    ends without one, so the next tap was ignored as part of the scroll.

  • Datepicker's previous and next month buttons have accessible names. The
    previousMonth and nextMonth i18n strings were never read; they now label
    the buttons and default to 'Previous month' and 'Next month'.

  • A drag that starts on a bottom sheet's .drag-handle now moves the sheet
    wherever the handle sits. The drag check looked only for the pre-1.0
    .handle class.

Removed

  • The version resolver's JSON output (scripts/resolve-version.mjs in the
    skill and the MCP package) drops declaredVersion,
    skillContractVersion and matchingCommit. The first two copied
    declaredRange and contractVersion, which remain; matchingCommit
    was always null. Read declaredRange and contractVersion instead.
  • Datepicker's undocumented formats property, the table of format
    functions. The format option and its tokens are unchanged.
  • Breaking (0.16.0). Most names kept for markup written before 0.8.0 are
    gone. Each one has a replacement that has been documented since 0.8.0;
    see Migration below. A few stay: .breadcrumb and .breadcrumb-wrapper,
    a bare ul.pagination, and checkbox input + span, which multiple Select
    still emits.
  • Breaking (0.16.0). The remaining compatibility paths are gone too: the
    .range and .range-field slider hosts, a switch whose .switch class
    sits on a wrapper around the label, the .extra size on a common button,
    radio input + span and switch input + span / .lever drawing, the
    li.tab wrapper, a chip's .active, Carousel's .flat layout, fullWidth
    option and .carousel-fixed-item, and the background, on-background,
    surface-variant and surface-tint color roles with their -light/-dark
    pairs and utility classes.
  • Selectors and code that never matched or never ran. The date and time
    picker rules no longer carry hyphenated twins of their generated parts,
    such as .date-picker-container; the script only ever emitted the
    unhyphenated names. The root .date-picker and .time-picker classes
    still start the components. Also gone: the .tabbed focus class, which
    every rule already paired with :focus-visible; the .halfway-fab,
    .searchbar.has-sidebar and .prefix ~ .chips offsets; the
    picker footer rules for buttons the script never created; and the
    normalize rules for h1, b, strong, small and kbd, which the
    typography rules already override.
  • Datepicker's startRange and endRange options. Nothing set them, and the
    is-startrange, is-endrange and is-inrange classes they produced had
    no styles. Use isDateRange for range selection.

Changed

  • The expressivecss skill is version 0.12.0 and expressivecss-review is
    version 0.1.1. A review now leaves the reviewed files unchanged and only
    offers its fixes. expressivecss-migration ships at 0.1.0.

  • Chips' form-associated control moved to a shared core module, which the
    cascader uses too. The control gained checkValidity() and
    reportValidity().

  • An open menu is a popover in the browser's top layer. This covers Menu,
    Select, Autocomplete, the menubar, Chips suggestions and the date
    picker's menus. A container that clips or scrolls no longer cuts a menu
    off, a modal dialog or sheet no longer covers it, and it draws above
    every z-index, so --expressive-z-menu now only orders the docked date
    and time pickers. Menu no longer sets position: relative on the
    nearest clipping ancestor. The menu's popover attribute is manual,
    and Menu still handles outside clicks and Escape itself. A context menu
    is kept inside the window rather than its scrolling container, and stays
    where it opened when that container scrolls. A coverTrigger: true
    menu, or a menu whose trigger sits under a transformed ancestor, is
    placed by measurement and follows its trigger when the page or a
    container scrolls. Firefox would anchor a top-layer menu to the
    trigger's box before transforms, which puts it beside a floating toolbar
    centred with translateX(-50%) instead of under its button. Migration:
    tests that run Menu in jsdom need showPopover and hidePopover stubs,
    and CSS or script that read an open menu's inline display: block
    should use menu[id]:popover-open or the instance's isOpen.

  • An open Lightbox shows its overlay, image and caption as manual popovers
    in the browser's top layer. The image still zooms from its thumbnail
    and back, and Tab, Escape, a click and scrolling still close it; focus is
    not trapped. Lightbox no longer sets overflow: visible on every
    ancestor of the image while it is open, which let a scrolling container
    lose its clipping. --expressive-z-lightbox no longer applies to it. The
    image carries popover="manual" only while open. Migration: tests that
    run Lightbox in jsdom need showPopover and hidePopover stubs.

  • The .mx-*, .my-*, .px-* and .py-* helpers set margin-inline,
    margin-block, padding-inline and padding-block. Horizontal text
    renders as before. In a vertical writing mode, .mx-* and .px-* now
    space the top and bottom, following the text, and .my-* and .py-*
    the left and right.

  • Menu, navigation bar and slider state layers read the
    --md-sys-state-*-state-layer-opacity tokens instead of fixed 8% and
    10% mixes. The defaults render the same. A page that overrides a state
    token now changes these layers too.

  • Every component inherits one Component.init, and the base constructor
    merges defaults with the options passed in. init types are
    unchanged. The CommandPalette and Questionnaire constructors accept
    any HTMLElement, which their init already took, and a subclass that
    overrides defaults now gets its own defaults merged.

  • Every component inherits one getInstance from Component, which also
    sets and clears the el.Expressive_<Name> property. The types are
    unchanged, except that Slider and CharacterCounter accept any
    HTMLElement. A subclass that overrides getInstance with its own return
    type no longer compiles (TS2417); delete the override, because the inherited
    one already returns the subclass.

  • The bundle uses ResizeObserver and IntersectionObserver without
    checking that they exist. Every supported browser has both, but tests that
    run components in jsdom need stubs for them, as they already need one for
    matchMedia.

  • A click in a Select sets selected on the option clicked and leaves the
    others alone. It used to write every option.

  • Data tables and line charts inside a shadow root sort and format numbers
    in the host's lang, as the date and time pickers already do.

  • The MCP package ships scripts/bounded-file.mjs and
    scripts/semantics-rules.mjs. Inspection read errors use new wording,
    such as "inspected file exceeds N bytes".

  • A runtime tooltip that opens while a tooltip is closing, or within 300 ms
    after that one's exitDelay ends, shows without waiting its
    enterDelay, so moving along a toolbar shows each tooltip at once. Only
    pages that set enterDelay above 0 see a difference. The first tooltip
    still waits its delay, and a tooltip that stays open does not shorten the
    others'. The window is fixed and has no option.

  • A Chips field whose text <input> has a name now submits its chips
    under that name, one id each, instead of the text being typed. A page
    that read the draft text from the form should read chips.getData(), or
    take the name off the input to keep the old behavior. The input now sits
    inside <expressive-chips-control>, so .chips > input selectors and
    input.parentElement code need updating. The input's validity,
    validationMessage, :invalid and invalid event no longer report the
    required state; read them from the control or the form. form.elements
    lists the control under the field's name. In a Questionnaire step, or on
    a .validate input, an empty required chips field does not count as
    invalid.

  • Datepicker and Timepicker follow the page's language. With a lang on
    the input or around it, the calendar takes that language's month and
    weekday names from Intl and its first day of the week where the browser
    knows it, and the clock defaults to 12- or 24-hour as that language does.
    Options the page passes still win, and the button words stay English
    unless passed. A page with no lang is unchanged. To keep the English
    calendar on a page with another lang, pass the English names in i18n.
    A typed date in the picker's own format is now read with its names, so
    the localized value it writes reads back, and 05/10/2026 in
    dd/mm/yyyy is 5 October rather than what Date.parse guessed.

  • A full-screen dialog.search-view opens on a click, Enter or ArrowDown
    in the app bar's search field, not on focus. Tabbing through the bar used
    to open the modal each time, so every keyboard pass needed an Escape
    (WCAG 3.2.1). A docked view still opens on focus. The bar shows the
    shared focus ring while its field is focused, since its input clears its own.

  • Snackbars queue. A new snackbar waits until the one on screen is dismissed,
    then shows, as Material asks, instead of replacing it at once. Migration:
    pass replace: true where a snackbar should still take over immediately,
    as copy buttons now do. A queued snackbar fires expressive:open and
    captures focus when it appears, dismissAll() clears the queue too, and a
    snackbar built from snackbarId has no el until it shows. A persistent
    snackbar whose container a re-render removed no longer blocks the next.

  • Time picker hours step round the day: in 12-hour time, Up from 11 AM
    gives 12 PM and Down from 12 PM gives 11 AM, so AM and PM follow the
    hour.

  • Radar chart gaps: a missing value breaks the outline instead of joining
    the spokes either side, as a line chart's does. With .area, each
    unbroken run fills as a wedge from the middle, drawn by a new
    .radar-chart-area path, and a value between two gaps is a dot.

  • A sheet drag that moves against the dismissing direction now counts as
    a drag, so the click that ends it no longer activates a handle button.

  • The time picker's hour and minute fields are spin buttons
    (role="spinbutton" with their range). Up and Down step them by one and
    Page Up and Page Down by five, wrapping round the dial, and the clock hand
    follows. Code that found them by the textbox role needs spinbutton.

  • The Tooltip plugin anchors its bubble with CSS anchor positioning where
    the browser supports it: the tooltip stays beside its activator through
    page and container scrolling and flips at the viewport edge. Anchored
    tooltips fade in without sliding, and transitionMovement joins the gap.
    Other browsers keep the measured position.

  • Menus opened from a trigger, including Select, Autocomplete, the menubar
    and the date picker's menus, anchor with CSS anchor positioning where
    supported: they follow the trigger through scrolling, flip at the viewport
    edge and scroll when taller than the room on that side. Context menus and
    coverTrigger: true keep the measured placement.

  • The tabs indicator moves on the default spatial spring
    (--md-comp-tabs-indicator-easing and -duration), and now stops under
    reduced motion. The Tabs duration option defaults to null; a number
    keeps the old ease-out timing for that instance. Switch handles slide on
    the fast spatial spring (--md-comp-switch-motion and -motion-duration),
    and the chip set's border and shadow change on the fast effects spring.

  • Button groups and the keypad press on the fast spatial spring tokens
    instead of the hand-sampled $motion-fast-spatial curve, through
    --md-comp-button-group-motion and --md-comp-keypad-motion with new
    -motion-duration pairs, so motion="expressive" reaches them. The press
    settles in 182ms instead of a 200ms window. $motion-fast-spatial stays
    for Sass that still names it.

  • Reduced motion now reaches every component through the tokens: under
    prefers-reduced-motion: reduce, all --md-sys-motion-duration-* and
    spring -duration tokens are 0s. Lists, the data table, the navigation
    bar, the mega menu, checkboxes, radios, pagination, tabs, the
    questionnaire and the lightbox had no rule of their own and animated
    before. No !important, so a page can opt an element back in.

  • Bottom and side sheets slide in and snap back on the default spatial
    spring (--md-comp-bottom-sheet-motion, --md-comp-side-sheet-motion and
    their -motion-duration tokens) instead of a 280ms ease, so
    motion="expressive" applies to them. Each sheet now extends 24px past its
    docked edge, with matching padding, so a spring's overshoot never uncovers
    the page.

  • The navigation rail expands and collapses on the default spatial spring
    (--md-comp-nav-rail-motion and -motion-duration). Its width, min-width
    and padding no longer follow --md-comp-nav-rail-duration, which now
    times only the indicator color.

  • FAB menu actions move on the fast spatial spring and fade on the fast
    effects spring. --md-comp-fab-menu-easing and -duration now default to
    the spatial spring, and the new --md-comp-fab-menu-effects-easing and
    -effects-duration time the fade.

  • Basic dialogs now animate in: they fade on the fast effects spring and grow
    from 96% on the default spatial spring (--md-comp-basic-dialog-easing,
    -duration, -effects-easing and -effects-duration). They still close
    at once. Sheets, dialog.max and the expanding card's dialog are
    unchanged, and reduced motion opens without moving.

  • The expanding card morphs on the slow spatial spring:
    --md-comp-expanding-card-motion-duration and -motion-easing now default
    to it instead of 500ms on the standard curve. New -content-duration,
    -content-easing, -fade-duration and -fade-easing tokens time the close
    button and detail content. Close cleanup still follows the actual clip
    transition.

  • Headings balance their lines and paragraphs avoid a one-word last line
    (text-wrap-style: balance and pretty). The longhand leaves wrapping
    itself alone, so .truncate and .text-nowrap still work.

  • A link without a class inside a p, dd, blockquote, figcaption or
    td is underlined, so links in running text no longer rely on color alone
    (WCAG 1.4.1). Links with a class, and links in lists, menus and navigation,
    are unchanged.

  • The documentation's category rail opens each group's pages in a side
    panel on hover or click, with each section as a dropdown. The section
    holding the current page starts open. The panel replaces the page list that
    sat beside the rail from Large up.

  • Snackbar's generated close button holds its icon in a
    <span class="material-symbols"> instead of an <i>. Styles that targeted
    .snackbar i need .snackbar .material-symbols.

  • Swipeable tabs no longer use Carousel. The panels move into a full-width
    .tabs-content scroll-snap track, so touch and trackpad swipes are native
    scrolling. Only the shown panel is reachable by keyboard; the others are
    inert. The track re-aligns when its width changes, so a tab bar started
    in a closed dialog or sheet shows the current panel once opened. Dragging
    the panels with a mouse no longer switches tabs; click the tab instead.

  • Components that read surface-variant now read
    surface-container-highest, as Material 3 maps it: the unselected switch
    track, the time picker's clock face and hour and minute inputs, the date
    picker's header rule and focused month and year field, and disabled cards.
    In dark mode those surfaces are a little darker and more neutral. Rules
    that read background or on-background now read surface and
    on-surface, which resolve to the same colors.

Migration

  • Sass: @include z-depth("0"), btn-flat and btn-disabled no longer emit
    box-shadow: none !important. A stylesheet that relied on the flag to
    cancel its own higher-specificity or inline shadow needs the flag itself.
    The .z-depth-0 class keeps it. Because the utility layer outranks those
    resets, .hoverable on a text button now lifts it on hover, and a pressed
    .filled or .outlined card with .hoverable keeps the hover shadow.

  • Icons: write <span class="material-symbols" aria-hidden="true">. A bare
    <i> is no longer treated as an icon inside buttons, lists, menus, chips,
    bars or fields, and .material-icons is gone; use .material-symbols
    (or the -outlined, -rounded and -sharp variants).

  • Field icons: name the side with .prefix or .suffix. The positional
    i:first-of-type / i:last-of-type fallback is gone.

  • Visibility helpers: replace .hide-on-small-only and
    .hide-on-small-and-down with .hide-on-compact-only, .hide-on-med-only
    with .hide-on-medium-only, .hide-on-xxl-only with
    .hide-on-extra-large-only, .show-on-small with .show-on-compact, and
    .show-on-xxl with .show-on-extra-large.

  • Replace .no-padding with .p-0 and .center-block with .mx-auto plus a
    display: block of your own. .no-select has no replacement class; set
    user-select: none in your own CSS.

  • Progress: replace .preloader with .progress.circular.

  • Date and time pickers: AutoInit starts .date-picker and .time-picker
    only. Rename class="datepicker" and class="timepicker" on the inputs.

  • Search: replace .searchbar with .search-bar.

  • Sheets: replace dialog.bottom with dialog.bottom-sheet, and the sheet's
    .handle with .drag-handle. Replace side sheet dialog.right and
    dialog.right-sheet with dialog.side-sheet, and dialog.left and
    dialog.left-sheet with dialog.side-sheet.start.

  • Toolbar: replace host .max with .docked and host .filled with
    .vibrant. A .filled action and a .max spacer inside the bar are
    unchanged.

  • Tooltip: replace .tooltip.max with .tooltip.rich.

  • Menus: replace a bare <hr> between items with
    <li class="divider" role="separator">.

  • File input: the trigger is a <label class="button"> wrapping the
    <input type="file">. A <button> or .btn trigger is no longer styled.

  • Script: replace Expressive.Range with Expressive.Slider.

  • Buttons: replace class="button extra" on a common button with
    class="button medium". Its label grows from label-large (14px) to
    title-medium (16px). circle extra is still the FAB. A navigation
    rail FAB is a plain .button.

  • Radios and switches: drop the <span> or .lever after the input; the
    input draws the control and the label text sits beside it. Put .switch
    on the <label> itself: <div class="switch"><label> now draws a
    checkbox.

  • Sliders: replace a .range or .range-field host with .slider, or wrap
    the input in a <label>.

  • Tabs: put the <a> links directly in nav.tabs, without ul and
    li.tab.

  • Chips: replace .active with .selected.

  • Carousel: replace .flat or fullWidth: true with an M3 layout such as
    .hero or .uncontained. A .carousel-fixed-item child is now an
    ordinary item; position a fixed overlay in your own CSS.

  • Color: replace --md-sys-color-background with surface, on-background
    with on-surface, surface-variant with surface-container-highest and
    surface-tint with primary, in variables and in the .x / .x-text
    utility classes. Overrides of the -light/-dark pairs move the same
    way.