WireKit v2.45.0
Minor. The motion preference an application sets now reaches the whole library rather than one rule of it — nine of the ten reduced-motion blocks ignored the opt-out entirely, and forcing reduction reached no pseudo-element and clamped no delay, so a shimmer kept shimmering and a staggered reveal kept staggering for exactly the readers who had asked them not to. Alongside it, a round of keyboard and focus defects found by driving the components rather than reading them: a hover card that closed out from under a focused control, a popover that pulled focus back from wherever you clicked, a submenu whose keys fell through to the menu above it, and an inline editor that never closed. Five components stop baking in values only your call site knows, and two controls reserve a touch target of their own.
Added
-
<x-wirekit::otp-input>takesautofocus. A one-time-code screen was the only screen in an authentication flow that loaded without focus, and the two-factor blueprint had been handing youautofocusas copy-paste source the whole time — an attribute the component discarded. Only the first digit carries it, because a browser honors the first one in the document and ignores the rest silently. It is a plain HTML attribute rather than a scripted focus call, so it applies while the page is parsed and still applies under a policy that never lets Alpine start. See OTP Input. -
<x-wirekit::rating>takesclearable, so a rating can go back to no rating. Once a star was picked there was no way to zero: stepping down stopped at one and clicking a star wrote that star, so a reader who mis-clicked could change their rating but not withdraw it. The gesture is a second click on the chosen star, plus ArrowLeft below one — not a separate clear button, which would add a permanent extra control and an extra tab stop to every rating including the majority that never clears. Off by default: a group whose selection cannot be withdrawn is a legitimate design, and nothing changes until you ask. See Rating. -
Five components stop deciding values your call site owns. Profile takes
radiusandgap—radiusacceptsnav-itemas a value of its own, because that token is contextual and no fixed rung can stand in for it, which is what blocked a sidebar row from using the component at all. Icon gains a2xlstep abovexl. The link component gains a size axis written in Text's vocabulary rather than a new one, defaulting to inherit. Row and Stack takescaleto choose which spacing laddergapreads, because the two ladders are not interchangeable and looked it. Every default is today's value, so an upgrade moves nothing. -
Sidebar takes
side, so a column on the trailing edge is the mirror of one on the leading edge. A navigation column draws a rule on the edge that faces the content and points its collapse chevron the way the column will move. Standing on the right, both were backwards: the rule marked the page margin, and the chevron pointed into the panel it was closing. The value is logical rather than physical, so a right-to-left document mirrors with the writing direction instead of against it. It is a value of its own rather than something inferred from where the collapse control sits — a three-column shell puts the control at the start of a list on the left and of a panel on the right, so the inference would have broken one to fix the other. -
wk-rail-hide, for the content a collapsing column cannot speak for. A Sidebar row has always known what to do when the column narrows: the label goes screen-reader-only and the icon carries it. Anything you put in the header or footer slot — a brand name, a workspace switcher, a mode label — had no way to join in and wrapped instead, which made the brand row the tallest thing in a column of icons. It is a named class rather than an internal selector, because this is something page markup is meant to reach for, and it hides while the column is still animating rather than at the end, since hiding late is the flicker it exists to remove.
Changed
- A read-only status matrix is a table again, not a grid.
role="grid"replaces a screen reader's native table navigation — where a move announces its row and column header — with an application-widget model that owns its own keyboard behavior, and the read-only cell types have none to offer: the role was declared with no tab stop anywhere inside it, so the reader was told to navigate and given nothing to navigate with. The editable cell types keep the role and the arrow keys that go with it. Keyboard reachability is unchanged either way; that always came from the region's own tab stop rather than from the role. See Status Matrix. - Reduced motion quiets everything with one duration instead of two. The stylesheet carried two clamp values, and which one reached an element was decided by specificity and source order rather than by anyone. It is 1ms throughout — not zero, because some browsers fire no transition-end event at a zero duration and a leave transition can then strand its element in the page.
Fixed
-
wire:modelon Multi Select and Tags Input updated nothing, and it failed in the direction that reads as success. The pills and the chips confirmed the choice on screen while the bound property never changed, so a filter looked set and filtered nothing — found in a running application rather than by a test, because every test set the property directly. Both controls keep their value in Alpine and submit it through hidden fields, which emit no event a binding can hear, and the attribute you wrote was dropped before it reached any element at all. Both now expose their value to the binding: the property receives the full array on every pick and every removal. See Livewire Binding on either page. -
wire:modelon Combobox bound the search text instead of the selection. The attribute landed on the visible text field, which already binds the query, so the property was fed whatever was typed rather than the option behind it — the page's own Pitfalls section warns against exactly that, and the component was doing it by construction. The binding now reaches the chosen value; nothing changes for the:valueseeding pattern documented beside it. -
data-reduce-motion="no-preference"now reaches every rule, not one of ten. An application that set it to override the operating system got the blanket clamp released and every component-local rule left in force — a page half animated, which reads as broken rather than as partial. And two animations gated on the opposite preference could never run for a reader whose system asks for less motion, including the reader who then said "not in this application"; both now have a counterpart the opt-out switches on. See Integration. -
data-reduce-motion="reduce"now reaches pseudo-elements and delays. A pseudo-element is not a descendant and animation timing is not inherited, so the skeleton shimmer, the progress shimmer and the radial sweep kept running at full speed for an application that had explicitly asked to reduce motion. Delays survived too, and a one-millisecond animation behind a five-hundred-millisecond delay is the same motion, later. Every reduced-motion rule now has its forced twin. -
The resizable handle and grip honor a motion preference. Both fades sat outside all three motion states: the component's root is built from arbitrary-value utilities, which the blanket clamp deliberately does not match, and neither the handle nor the grip carries a class of its own. See Resizable.
-
A hover card stops closing out from under a focused control. Moving the pointer off a card whose button you had tabbed into closed it and dropped focus to the page body, so a keyboard reader lost their place with nothing announced. The card now stays while anything inside it holds focus, and still closes when nothing does. See Hover Card.
-
A popover stops pulling focus back when you click somewhere else. Clicking any other control closed the popover, correctly, and then returned focus to the popover's trigger — so the next keystroke arrived at a trigger you had just left, and a field you clicked into lost focus before you could type. See Popover.
-
Type-ahead in an open submenu stays in the submenu, and Space activates a row. Every key the submenu did not handle fell through to the menu above it, so a single letter pulled focus out of the submenu and onto a sibling of its own trigger. Space was missing entirely, which matters on a row that renders as a link, where the browser does not activate on it. See Dropdown.
-
A submenu built from radio or checkbox rows is no longer inert. It collected only plain menu items, so a submenu of
dropdown.radio-itemordropdown.checkbox-itemrows came back empty — and an empty list made the key handler return before doing anything, so every key including the arrows fell through to the level above. -
Clicking the image-compare divider focuses it. The component suppresses the browser's default on pointer-down, which also suppresses the focus it would have given the handle, so the arrow keys the component implements went nowhere after a click. See Image Compare.
-
The multi-select panel anchors to its field again when
optimisticis set. The nested optimistic scope captured the references the placement code looks up, so the panel was never positioned: no offset, no matched width, no viewport fit — measured six hundred pixels below the field and three hundred too narrow. See Multi Select. -
Picking a row in a sidebar listbox with the mouse no longer scrolls the column. A half-visible row at an edge was scrolled into view under the pointer, which moved a different row beneath the cursor — so a second click landed on something you had not chosen. Keyboard movement still scrolls, which is what the behavior was written for. See Sidebar.
-
A context menu navigates from where focus actually is. It moved from a remembered position instead, so any focus change the component did not make itself — a click, a restore after a re-render — left the arrow keys stepping from somewhere the reader no longer was. See Context Menu.
-
An inline editor with
control="select"closes when you click away. Every other control did; this one stayed open with an uncommitted draft until Escape or a confirm, which in a table of inline-edit cells left several open at once. See Inline Edit. -
Both range-slider thumbs announce a bound they can reach. Each reported the track's own limit while the control accepts one step less, so a screen-reader user heard a ceiling, drove toward it, and stopped somewhere else with nothing said about why. A range configured tighter than one step also reported a maximum below its minimum. See Range Slider.
-
tour.stephonors anaria-labelyou set. The attribute landed on the element the component had already named, so the panel carried it twice and the browser used the first — a step named for a screen reader had no way to show it had not taken. See Tour. -
The password strength meter offers no value text instead of an empty one. An empty
aria-valuetextis not the absence of a value text; it is a value text that says nothing, and it overrides the number a reader would otherwise hear. See Password Input. -
The reading-minimap anchors say "focused" with a shape rather than a second color. The focused state shared one rule with hover and changed nothing but the text color, so a keyboard reader saw exactly what a mouse reader sees when hovering, and a reader who cannot separate the two shades saw nothing at all. See Reading.
-
Radio and Toggle reserve a touch target of their own. Both hide the real input at one pixel square and paint a box beside it, so the thing a finger lands on is a span — and the coarse-pointer floor for fields and buttons never reached either. The reserve is an invisible expander rather than a size floor, so both controls keep the size they were designed at and nothing around them moves. The card variant of the radio is deliberately excluded, where the label is already the target.
-
php artisan wirekit:csp-auditscans a directive whose own name carries a colon. It stopped at the first one, so every namespaced event name went unread — including the convention this library tells you to listen for. The first run after the fix found a dead listener in the sample application, which is what the command is for. -
Five components now implement the keyboard model their page had been promising. Accordion documented arrow keys and Home/End between its headers over a plugin that bound no key at all. Range Slider documented six keys while both thumbs bound left and right only — and up, down, Home and End are required keys of the multi-thumb pattern rather than optional ones. Tags Input promised list semantics, a group role, announcements, a disabled state at the limit, Escape and a virtual caret. Table promised sortable columns in its Alpine path, where the header carried a click handler and neither a tab stop nor a button, so that mode was mouse-only while the Livewire mode beside it rendered a real button for exactly this reason. Multi Select promised a combobox and bound one key: you could filter but never choose.
-
Four dialogs opened and left focus behind, which is the one thing a dialog may not do. Color Picker's popover moved focus nowhere on open and dropped it on the page body at Escape. Tour puts its steps behind a full-screen backdrop and focused none of them, so the reader stayed on the trigger underneath it and had to tab the whole covered page to reach Back or Next. Navigation Menu's flyout was not in the tab order at all and announced a dialog for a panel with no role. Menubar took its arrow keys from whichever menu was open instead of the focused trigger, so pressing down on the second menu opened the first.
-
A teleported context menu carried none of the keys bound on its wrapper. Context Menu binds its key handler on its root while the panel is moved to the overlay root, so arrows, Home, End and Escape reached nothing once the menu was open and the first item was never focused.
-
Three ordinary bugs with a sharp edge. Calendar read a January value as no value, because January is month zero and a falsy check cannot tell those apart — a calendar pre-filled with a January date opened on the current month while the form submitted January. Dropdown swallowed Space on a focused item, because the type-ahead branch takes any single character and a space is one: Enter worked, Space did nothing, and the page documents both. Range Slider thumbs never took focus from a pointer, so dragging one and then pressing an arrow key scrolled the page instead of nudging the value.
-
A tooltip that has to be hoverable and dismissible was neither. The panel ignored the pointer, so a reader who moved onto it to finish reading lost it, and Escape was bound on the component rather than the window — under plain hover nothing inside the component has focus, so the key never arrived. See Tooltip.
-
Focus fell to the page body in four more places. Inline Edit treated any focus target other than its own control as leaving, so a mouse press on Confirm discarded the draft before Confirm could act — a button takes focus on press, and the blur ran first. Hover Card closed shortly after focus entered its panel, because its containment check asked the wrong element and the panel is teleported out of it. Reading's minimap put focusable anchors inside its own hidden subtree, and collapsed anchors kept focus while fully transparent. Tree View gave every node a negative tab index and nothing a positive one, so the tree could not be entered by keyboard at all.
-
A rating announced several selected stars at once. The server-rendered markup marks the chosen star; the live path marked every star up to it, so after the page came alive a three-star rating exposed three selected radios in one group — a shape no radio group can have. See Rating.
-
The file-upload dropzone showed no focus indicator. Its real input is visually hidden inside the label that draws the zone, and the zone carried no focus ring of its own, so a sighted keyboard user could not see when the control had focus. See File Upload.
-
The password strength meter said everything with color. Four bars, a status region with no text in it, and a description pointing at that empty region — while the page promised a meter role, a value, an announced level and a pressed state on the reveal toggle. Color alone is not information a form field may rely on. See Password Input.
-
php artisan wirekit:export-jsonand the schema file the installer writes carried no documentation link for any component in a real installation. The link resolves against the documentation tree, that tree is excluded from the package, and there was no fallback — so the file whose whole purpose is to feed an editor and AI tooling shipped with every link empty, and the defect cannot reproduce in a checkout of this project. It is proven now against an unpacked release archive rather than a working copy. -
The bundle guide that ships inside the package showed a directive form that does not compile.
@wirekitScriptstakes a content-security nonce; which bundle you get comes from configuration, and the guide named a bundle value that does not exist while leaving out the one that does. -
A notification center dialog had no name at all. A spelling pass had changed
labelledtolabeledinsidearia-labelledby— the one spelling that must not change, because the misspelled attribute is not an attribute: nothing throws, nothing warns, and the dialog is simply nameless. See Notification Center. -
A second wave over the remaining findings closed, component by component, a documented behavior no handler implemented — or an announcement naming an element the render never emitted. Among them: Data Table's empty state now announces through text that changes rather than a region that is merely shown — a visibility flip is not a content change every screen reader reports — and its column popover stopped announcing a menu it does not implement, becoming a named group with a real focus model instead. Among the rest: Command Palette, Image Compare, Lightbox, the sortable list, Breadcrumb, Combobox, Time Picker, Alert Dialog, Event Calendar, Avatar, Attachment, Skeleton, Product Card, Ticker, Progress and Usage Meter.
-
The application shells were walked end to end and rebuilt from what the measurements said. A collapsed sidebar row offered a pointer nothing at all — its label goes screen-reader-only and the icon carries it, so every name check stayed green while a reader holding a mouse saw a column of identical icons; the rail one component over had solved this with a tooltip and the sidebar had not. Rows with no icon emptied the column when it collapsed. Names in
profileandbrandwrapped instead of stepping aside. A footer's rule was welded to the box above it with no air and a wide gap beneath. The collapse control spent a whole navigation row on itself where riding on the footer band costs none. A container-wrapped Navbar sat one padding step inside the content edge every other page-edge component shares, because it carries its padding inside the cap while the main region carries it outside. The collapsed column was three pixels wider than the rail inside it. And two columns' feet started at different heights, so the line meant to cross the shell broke; they read one shared floor now. -
Every text field rendered its
nametwice. The attribute was written explicitly and then again from the pass-through bag, which is invalid HTML. Both copies held the same value, so no browser behaved differently — but a validator reports it, and a later change to the order in which the two are written would have flipped which one wins, silently. See Input. -
A link-shaped button ignored its own loading state. The disabled wiring was emitted whatever tag the component rendered, and a browser honors that on a button and ignores it on an anchor — so a link-shaped button looked wired and stayed fully navigable for the whole request, which is the one moment a second click does the most damage. The link branch now switches to the classes this component already uses for a disabled link, so the two disabled states are indistinguishable. See Button.
Documentation
-
Carousel explains where its movement keys come from. The keyboard table promises the four of them and the component binds none — the viewport is a focusable, mandatorily snapping scroll container, so the browser resolves an arrow-key scroll to the next slide. A developer who goes looking for the binding and finds none should learn that this is the design rather than conclude the table is stale.
-
Integration says which Alpine runs on a Livewire page, and names the setting that helps. Choosing the CSP bundle there buys nothing: Livewire's script is not deferred and starts its own Alpine while the page is parsed, so the kit's bundle registers against that one and never starts the parser you chose it for. The page keeps needing
unsafe-evaland carries more JavaScript than before. The lever is Livewire's owncsp_safe, which appeared nowhere. -
Animations no longer claims the reduced-motion rule matches your own
animate-*classes. It never has — the rule is scoped to WireKit's own elements, which Theming said correctly two pages away. -
Toast documents its
labelprop, the switch that turns the region into a landmark. Modal stops naming two props it does not have. Tooltip no longer demonstrates a zero hide-delay, which switches off the hoverability the same page requires. CLI Reference gives its repeated headings distinguishable names, so a deep link reaches the section it points at. -
Tabs stops promising a keyboard model the component deliberately does not have. It listed both arrow axes where only the one matching the orientation is bound, and an activation mode with no prop behind it — while a paragraph three screens up on the same page already said activation is manual. Corrected on the page rather than in the component, because the component is right on both counts: the pattern binds the axis its orientation names and treats the cross axis as optional. The pitfall about inactive panels was wrong in both halves and is rewritten — fields in a hidden panel do post, and the real hazard is a submit carrying values the reader never saw.
-
Integration stops telling CI to branch on an exit code no command returns. Every
wirekit:*command uses two codes, which the CLI Reference states on the same site; the example here branched on a third. Measured against the command rather than corrected by reading.