Getting started
Changelog
Every release of @lyeve-labs/ui-kit. Versions follow SemVer: while
we're under 1.0 the minor carries new components and new API, and any breaking change
too. The patch carries fixes. Nothing exported at 0.13.0 has been removed or renamed since.
0.60.1 Latest 2026-11-10
Changed
- A focused field shows focus by its border alone, which turns the focus color at its own 1px. The 1px ring inside it made a 2px edge, heavier than the 1px edge of a highlighted option in a list. A field in error or holding a suggestion keeps its red or violet border, so it still adds a 1px ring in that color inside the border while it has focus. This holds for
Input,Textarea,PasswordInput,SearchInput,NumberInput,Select,Autocomplete,DatePicker,ColorField,MultiSelect,CopyFieldand the search box at the top of a list.
0.60.0 2026-11-10
Changed
- An empty checkbox, radio or toggle rests on
line-strongat 60%, about 2:1 on every ground, and firms to fullline-strongunder the pointer. Fullline-strongdrew a gray frame round every box. This coversCheckbox,CheckboxGroup,RadioGroup,Toggle, the option boxes inMultiSelectand the boxes inPluginCart. - An option card in
Checkbox,CheckboxGroupandRadioGrouprests on the softlineborder and firms toline-strongunder the pointer. A selected card keeps its brand tint and no longer draws a brand border, so the filled box or dot inside it marks the choice. - A focused field draws one 2px edge in one color: the border turns the focus color and a 1px ring just inside repeats it. The 2px half-strength ring inside the border read as a second line. A field in error keeps one red edge while it has focus, where it drew a blue ring inside the red border, and a field holding a suggestion stays violet. This covers every text field, select and autocomplete,
NumberInput,MultiSelect,CopyFieldand the search box inside a list panel. - The
FileInputdrop zone shows focus by closing its dashes into a solid line, in the focus color outside an error, with no ring beside it.
0.59.0 2026-11-10
Changed
- Every field rests on the soft
lineborder aCardand aButtondraw and firms toline-strongunder the pointer:Input,Textarea,SearchInput,PasswordInput,NumberInputwith its steppers,Select,Autocomplete,MultiSelect,DatePicker,TimePicker,ColorField,CopyField, theFileInputdrop zone and the search box inside a list panel. Focus still moves the border to the focus color. A frame focused through a part inside it keeps its focus border under the pointer. Checkboxes, radios and toggles keepline-strong, because an empty one has nothing else to mark it. - The chosen
SegmentedControlsegment is a brand-tinted fill with brand text, drawn the way the sidebar draws the current page, instead of a raised chip with a thin brand ring. The track rests online.
Fixed
- A
Tooltipstays inside the browser window. A hint on a trigger near an edge moves along that edge to stay 8px inside it, and a hint with no room on its own side opens on the opposite side when that side has room.
0.58.0 2026-11-10
Changed
- The secondary and outline
Buttonvariants and theSelecttrigger draw the softlineborder aCarddraws instead ofline-strong. A button's label and a select's value and chevron identify the control, so the border does not have to. TheSelecttrigger firms toline-strongunder the pointer. Text fields, checkboxes, radios, toggles andSegmentedControlkeepline-strong, because their border is what marks them.
0.57.0 2026-11-10
Added
- A
focuscolor, the one color for every focus ring, focus outline and focused field border, used asring-focus,outline-focusandborder-focus. It isbrand-deepwherever the control sits, so a part of a page that sets its own brand draws its rings in that brand.
Changed
- Focus rings, the focus outline and a focused field's border draw in
brand-deepinstead ofbrand. In the dark palette a ring reads 5.59:1 on the page instead of 10.43:1, so a ring round a menu row or a chosen segment no longer outshines everything else on the screen, and it still clears 3:1 on every surface. A field's inner ring is the same color at half strength. A theme that setsbrandalso setsbrand-deep, or its rings keep the kit's color. - The row the keyboard is on in a
Dropdownfills like a hovered row under its ring. - The panels of
Dropdown,Select,Autocomplete,MultiSelectandDatePickerdraw the softlineborder thatAccountMenu,ModalandCarddraw. The shadow and the lit top edge set them apart from the page. - Scroll bars take the palette's colors.
checkPalettereports a focus ring problem underbrand-deep, the color the ring is drawn in, where it reportedbrand.
0.56.0 2026-11-09
Added
Cardtakes animage, drawn edge to edge above the heading and clipped to the card's top corners. With its own width and height it fills the card's width at its own shape, and without them it is cropped to a ratio, 16:9 unless the image names another.NavBar,FooterandGalleryfor a page drawn for visitors, rendered on the server with no script and part ofsite.css. The navigation bar's links wrap onto more lines on a narrow page instead of hiding behind a menu, and it marks the current page. The footer names each social profile by its network as a plain link. The gallery crops its images to 4:3 in a grid, or in a row that scrolls sideways, stops on each image and takes keyboard focus.- Components for a page drawn for visitors, each rendered on the server with no script:
Section, a band with an introduction, content and a footer, an optional background image and a light or dark overlay,Columns, which stack once a column would fall under the site's minimum width and take an optional cap of two, three or four,Prosefor markup written elsewhere,Image, which reserves its box from its width and height or a ratio, andQuote. Each is part ofsite.css. data-scheme="dark"or"light"on any element draws everything inside in that palette, whatever the page's theme. ASectionwith an overlay names the overlay's scheme, so its body text, muted text and links clear 4.5:1 over any image.- Site tokens the new components read:
--spacing-site-xsto--spacing-site-xl, set from--spacing-site-md,--spacing-column-min,--spacing-gutter, and--color-brand-2, a second color that followsvioletuntil a site sets its own. FormAssistwraps a form's fields so an assistant on the page can read them and suggest a value for each. A suggested value goes into its control, drawn in violet with a Suggested tag and buttons to accept or reject it, and a bar above the fields accepts or rejects them all. While a suggestion waits, the form does not submit and the bar asks the person to review it first. A value the person changed is never overwritten, and a password, a file and any control givennoAIare never reported or filled. A suggestion a field was holding when it leaves the page, behind a tab or a closed section, is rejected and the old value comes back.activeAssistForm()returns the form to work on, the one in an open dialog or drawer before the page's, through a controller that can describe, suggest, accept all, reject all and list what is pending.assistField()registers a control the kit does not ship.- Every field control takes
noAI, which keeps its value from an assistant and marks itdata-no-aifor a browser agent. Toggle,MultiSelect,DatePickerandFilterChipstakenameand post their value with the form.AppShelltakes apanelon the right, opened withpanelOpenand named bypanelLabel. From thelgbreakpoint it docks beside the main area, which narrows to make room, and its edge drags or steps with the arrow keys between 320 and 560 pixels, remembered underpanelStorageKey. The cap shrinks so the main area keeps at least 480 pixels. Below that, the same panel opens as a modal drawer from the right, so its content stays mounted as the window crosses the breakpoint, and a panel that has never opened renders nothing.
Changed
- Every kit radius rung,
xsto2xlandcard, is a multiple of--radius-md, so a page that sets it once moves the whole scale. At its default of 8px every rung keeps its value. SectionHeadingat both levels and the title of aCarddraw in--font-heading, a new token that is the body face unless a page sets its own.- Breaking:
Accordionis built ondetails, so the browser opens and closes each item and an accordion works on a page that runs no script. Its items share a name unlessmultipleis set, which has the browser close the others when one opens. An item's body fades in at each opening and shuts in one frame, where it used to grow and shrink to its height. The header is the item'ssummary, which holds the title as plain text, and it is no longer anh3around a button witharia-expandedandaria-controls. A heading inside a summary hides the item's state from VoiceOver on iOS, drops out of heading navigation in JAWS and TalkBack, and keeps the item shut when a reader presses Enter on it in browse mode with Orca in Firefox. The titles are no longer in the page's outline, so put a heading above the accordion to name the group. The body has no region role, and theaccordion-header-andaccordion-panel-ids are gone, because the browser reports an item's state itself. An item'sidis now the id of itsdetailselement, unique on the page: find an item by it and read itsopenproperty. A page put together from several server renders gives each its ownidPrefix, or two accordions on it share a name.
Fixed
- The account menu's panel never played its entrance in Chromium, which hides a shut panel without dropping its style, so the animation ran once while the panel was hidden. It now plays at each opening. A closed menu inside an open part of the page no longer draws its button lit and its chevron turned.
0.55.0 2026-11-09
Added
SplitPanetakes aheadersnippet in place of its label in the header row, for a pane whose own tabs already name what it holds. The label still names the pane to a screen reader, on the strip and on its controls. The row stays one control tall, and in a pane dragged narrower than its tabs they scroll in their own strip while the lock and the fold stay in place.TabstakesinHeaderfor a strip in such a header row: it draws no rule of its own and its tabs take the row's height.
Changed
- A
SegmentedControlkeeps each label on one line. When its options do not fit beside each other, in a narrow panel, a flex row or a grid column, it stacks them, and it lies back down once there is room. - A
Cardclips its content to its corners only withpad="none", so a list that opens from a field in a padded card draws over the page instead of growing the card. The footer band rounds its own corners. AccountMenucloses when a link in it, or a button that posts a form, is chosen, and gives focus back to its trigger. A click canceled on the row before it reaches the menu leaves it open.
Fixed
- A
PageShellwithfillno longer scrolls the shell when a visually hidden label sits in a scrolling region inside it, and every scrolling region in the kit is positioned so such a label stays inside it. - A panel opened from a field (
Select,DatePickerand the rest) stays inside the room the reader can see, inside any scrollbar of the region that clips it. When it runs past an edge it aligns to its field's other edge first, then slides back with an 8px gap. A panel that fits is left aligned with its field. - A
NumberInputstepper at its bound fades only its glyph and keeps its frame, where it drew a dark block. DatePickergives focus back to its field when a pick or Escape closes the calendar with focus inside it, with a focus ring only after a keyboard pick.
0.54.0 2026-11-08
Added
@lyeve-labs/ui-kit/site.cssis the stylesheet compiled for a page with no Tailwind build: the theme in all three schemes and every class of Accordion, AccordionItem, Badge, Button, Card, Checkbox, Field, FormMessage, Input, Label, SectionHeading and Textarea.
Changed
- Every
Buttonvariant carries a 1px border, transparent where none shows, so every variant is one height at each size: 34px forsm, 38px formdand 46px forlg. A filled button drew 2px shorter than an outlined one, andmdnow matches the 38px height ofInput. In forced colors aghostbutton shows its edge.
Fixed
- The close button of
DrawerandModalis a 24px target under a mouse, up from its 16px glyph, and the glyph stays where it was. CopyFieldwithwrapbreaks a value after a hyphen, and inside a token only when the token is wider than the line, so a UUID no longer splits inside a group. The copied value is unchanged.- The built package refuses a tool's cache, so a
node_modulesor.vitefolder left under the sources no longer reaches the published files.
0.53.0 2026-11-08
Added
StepIndicatortakeslabel, the list's accessible name, which defaults to "Progress". Name each list when a page holds two.data-theme="system"on<html>paints the light or dark palette the operating system asks for, with no script, and repaints when that setting changes.getTheme()reads it the same way. A light override for such a page goes inside@media (prefers-color-scheme: light), as the README shows.
Changed
- The close button on
Alert,Bannerand each toast is named "Close" instead of "Dismiss". - Default labels name what a control acts on. With a
label, the clear button onAutocompletereads "Clear" and the label, theNumberInputsteppers read "Decrease" and "Increase" and the label, andCopyFieldkeeps an acronym such as "API key" in its button name rather than lowercasing it. Without one they keep the bare verb. - Default placeholders are plain words without an ellipsis: "Search" on
AutocompleteandSearchInput, "None selected" onMultiSelectand "Choose a date" onDatePicker, whose dialog is also named "Choose a date". FileInputreads "Choose a file or drop it here", or "Choose files or drop them here" when it takes several, in place of "Click to upload or drag & drop".- The
AppShellrail toggle reads "Expand navigation" and "Collapse navigation" in place of "Show sidebar" and "Hide sidebar". PluginCartreads "No capabilities chosen yet. With none, you stay on the Free plan." when nothing is chosen, names each remove button "Remove" and the capability, and shows "Waiting for a price" without a period.- A copy that fails says "Could not copy. Try again." on
CopyButton, in place of "Copy failed".CopyFieldsays "Could not copy. Select the value and copy it yourself.", or "Could not copy. Show the value, then select it and copy it yourself." while its value is masked. DatePickershows the chosen date as "Sep 3, 2024" in every browser, where it followed the browser's locale, and its month grid always holds six weeks, so the panel keeps its height from month to month.Dropdown,AccountMenuandDatePickerclose when a press lands outside them, on pointer down rather than on click, and listen for it only while open.Labeldraws the same label as every field: sentence case at the body size, where it was small uppercase.- The
Cardfooter takes the header's 16px vertical inset, where it had 12px. Statdraws its trend arrow as an icon and says "Up", "Down" or "Unchanged" to a screen reader.Button's spinner is 14px or 16px, the icon sizes beside its text, in place of 15px or 18px. The cross on a removableTagis 12px, where it was 10px.- The close and chevron marks across the kit are drawn from
@lucide/svelte. - The count on a
Collapsibleis drawn as aBadge, so it gains a border and medium weight. - The
Drawerclose button sits 2px lower, level with theModalone. AppShell,GateNotice,SectionHeading,TableandTabsno longer carry thedata-testidvaluesapp-section,app-sidebar-toggle,gate-notice,section-actions,table-frame,table-scrollandtabs-scroll. Select those elements by role or name.
Fixed
- A
FileInputwith noidgets one of its own, so its label names the input. - The
Drawerclose button draws the same rounded focus ring as theModalone. SortableListlistens for pointer events on the window only while a row is pressed or dragged.
0.52.0 2026-11-05
Added
motion.swapPage(), for a router to call just before it replaces the page. Every motion preset plays when an ancestor removes its surface, and the page is one, so a page holding anAlert, aBanneror an open overlay stayed on screen until that exit ended, with the new page below it. AfterswapPage()every preset plays in no time until the next frame, so the new page takes the old one's place at once.checkPalette, which runs the contrast rules the kit's own palettes pass over an app's own colors. It takes the tokens an app overrides for each theme, lays them over the kit's values and returns every pair below its WCAG 2.2 floor. It measures text on the grounds the kit paints, from the center of each canvas pool and the hero to the current, head and held rows and the danger panel. It measures badges, avatars and row chips, and a filled button's label at rest, hovered, held and under its lift. At 3:1 it measures the focus ring, a control's border and a hovered button's edge. Each result names the theme, where the pair is painted, the two tokens, the ratio and the floor, and an empty list means the palette passes. It reads#rgband#rrggbb, and it throws on any other format, an unknown token and an unknown theme.
Fixed
SidebarNavopens a group the current page moved into, and shuts one it left, in one frame. Only a group the reader opens or shuts slides, so a page change no longer moves the rows under the pointer.- A page printed in the light theme kept an app's own colors whenever the app set its light palette in a rule after the stylesheet, as the kit's docs show. The print palette's values are now important, so such a page prints black on white.
- The README's palette override set only the dark theme, so a light page kept the kit's colors. It now sets both, with colors that pass
checkPalette.
0.51.2 2026-11-04
Changed
- The dark palette's raised layers mix 2% of the dark blue (
#003366) into the navy, in place of the 5% of 0.51.1, for a softer blue.surfaceis#0c1728,surface-2is#121e30andlineis#192a3d. The line sits one step above the exact mix so it stays 1.23:1 against the surface.ink, the brand colors and the light palette are unchanged, and every text and control border pair still clears WCAG AA.
0.51.1 2026-11-04
Changed
- The dark palette's raised layers are navy with 5% of a dark blue (
#003366) mixed in, in place of the muted blue of 0.51.0, which leaned toward cyan.surfaceis#0b172a,surface-2is#111f32andlineis#192a3f. They keep the lightness of the navy in 0.48.0 with the hue moved slightly away from violet.ink, the brand colors and the light palette are unchanged, and every text and control border pair still clears WCAG AA. 0.51.0 was not published to npm.
0.51.0 2026-11-04
Changed
- The dark palette's raised layers are a muted blue instead of gray. They hold less color than the navy of 0.48.0 and lean further from violet.
surfaceis#0b1723,surface-2is#121f2bandlineis#1c2938. Each keeps the lightness it had, andink, the brand colors and the light palette are unchanged. Every text and control border pair still clears WCAG AA.
0.50.0 2026-11-04
Added
AccountMenuItem, one row ofAccountMenu, drawn like a restingSidebarNavrow: an optional 16px icon in its own column and a muted label that turns to full text color on a surface-2 fill under the pointer, with no border, shadow or fill at rest. Withhrefit is a link and passestargetandrelthrough. Without one it is a button,type="submit"for a sign-out form.loadingdisables the button, marks it busy and shows a spinner in place of the icon.AccountMenutakes an optionalfootersnippet, drawn under a rule after the rows, for the form that ends the session. The rows and the footer are spaced like the sidebar's rows.
Changed
- The
AccountMenutrigger keeps its hover fill while its panel is open, so the panel reads as coming from it.
0.49.0 2026-11-04
Changed
- The dark palette's raised layers are gray instead of navy, so a card no longer reads violet.
surfaceis#11171d,surface-2is#191e25andlineis#222931. Each keeps the lightness it had, andink, the brand colors and the light palette are unchanged. A hovered table row still reads apart from the head row and from a resting row, and every text and control border pair still clears WCAG AA.
0.48.0 2026-11-04
Changed
- The dark palette's raised layers are darker, so a card no longer reads gray against the page.
surfaceis#0c1627,surface-2is#121e2fandlineis#1a293c. A hovered table row still reads apart from the head row and from a resting row, and every text and control border pair still clears WCAG AA. - The dark card no longer carries a white wash at its top.
bg-cardisnonein the dark palette, so a card is its plain surface color. The light palette's card is unchanged. - The
highlightalong a raised surface's top edge is half as bright in the dark palette, white at 6% instead of 12%. AuthShellsits on the same ink andbg-canvaslight as the app shell's content, so a sign-in page matches the console and the sites instead of a gray-violet wash.- The hero wash under
EmptyStateis half as strong, 5% of violet into the ink instead of 10%.
0.47.0 2026-11-04
Fixed
SidebarNavandBreadcrumbrefuse an item's href that a browser would run as script, the way the kit's other link components do. That covers ajavascript:,data:orvbscript:URL, including one with leading whitespace or a tab or line break inside the scheme, whereBreadcrumbused to catch only the first two written plainly. The item still renders, without a link. A relative orhttpshref renders as given.- A hovered
Tablerow takessurface-2, so it no longer reads as a second head row and is no longer outlined by a ring that crossed the frame's rounded corners. A row of header cells only takes no hover.
Changed
- Focus no longer glows. A focused button, link, card, tab or menu row shows its 2px brand outline or ring and nothing around it.
- A focused text field shows its brand border with a soft ring inside it, at 35% of the brand, and no glow outside it. A date or time segment and an icon button inside a field keep the full brand ring, because nothing else marks their focus. The file drop target now turns its border brand on focus, as the other fields do.
- The primary button no longer sits in a pool of light or grows a glow on hover, press or focus. It keeps its fill, its hover and press colors and its top bevel, casts the same small shadow as the other filled buttons in every state, and the light over its fill is fainter. The glow and primary shadow tokens stay defined, and the two primary ones now hold the raised shadow.
- A clickable card answers the pointer with its brand border alone, with no glow.
- A nested sidebar row is drawn exactly like a top-level row at rest, under the pointer, on focus and as the current page. Its fill and its current bar now start at its own indent instead of in its section's column, and the current row no longer carries an edge a hovered row lacks. One row is marked at a time. While the section that holds the current page is open, the page's row carries the mark, and the section shows its label in full text color with no bar of its own. Once you close that section, the section carries the page's mark until you open it again.
- A
Table's head row takes the newbandtoken, the brand at 8% in the surface, slightly deeper than before. - The
Tableframe drops the card's top wash, so its rows are one color at rest.
0.46.0 2026-11-03
Added
- Depth tokens in both palettes, each with its Tailwind utility: the colors
highlight,shade,glow,haloandtint, a shadow ramp fromshadow-raisedthroughshadow-cardandshadow-popovertoshadow-overlay,shadow-glow,shadow-mark,shadow-primaryandshadow-primary-hover, the insetsinset-shadow-highlight,inset-shadow-bevelandinset-shadow-well,rounded-card,leading-title, and the background imagesbg-canvas,bg-sidebar,bg-card,bg-hero,bg-lift,bg-accent,bg-accent-y,bg-rule-endandbg-rule-y.
Changed
- Surfaces carry depth in the same palette.
Card,Panel,StatandTablesit on the card shadow with a lit top edge, the 20px card radius and a gentle gradient laid over their background color, so a background a page passes still shows. Menus, tooltips, toasts and a draggedSortableListrow take the popover shadow, andDrawerand the dialogs the overlay shadow. AppShelllights the top of its content column with a faint cyan and violet wash, gives the sidebar its own gradient and a right edge that fades at its ends, and draws the header over the wash with a rule that fades toward its end edge.- The primary
Buttonhas a vertical gradient, a bevel and a brand glow at rest that grows on hover. Every focus ring glows cyan, and fields andSegmentedControlsit in a shallow well. - The current
SidebarNavrow has the brand tint, the cyan to violet accent bar and a faint glow. The active tab inTabsand theProgressfill carry the same accent, and every horizontal accent is mirrored in right-to-left layouts. - A
Stattile carries a 2px cyan to violet line along its top edge, and aTableheader row and a hovered row take the brand tint. ABadgedot carries a soft halo of its own tone. EmptyStateandAuthShellsit on a hero wash of ink into a tenth of violet.- The
PageHeadertitle is 32px on a 38px line, semibold with tighter tracking, and 24px below thesmbreakpoint. Section eyebrows step up to the muted color and semibold. - Every addition is a shadow or a background image, so nothing changes size and nothing shifts. Forced colors and print drop the gradients, glows and shadows, and every text pair the change adds clears WCAG AA in both palettes.
Fixed
Progressstays visible in forced colors. The track draws a border and the fill paints in the system text color, where both used to vanish.- The primary, violet and danger buttons show their boundary in forced colors, with an outline that takes no space. They had none.
0.45.0 2026-11-01
Added
PluginCarttakescharged, the price in cents the order will charge. A host that prices a discount code apart from the cart's quote passes it, and while the quote still describes the selection it stands in for the quote's total in the sticky bar and the default summary, and is the total thesummaryandactionssnippets receive. Without it the cart reads the quote as before.
Fixed
Tabsis one stop in the tab order. The selected tab holds it, Left and Right move the focus along the strip and wrap at the ends, and Home and End go to the first and last tab. Moving does not choose a tab: Enter, Space or a click does.- Between the
mdandlgbreakpoints theAppShellrail opens only once a pointer has rested on it for 100 ms, so a pointer crossing it toward a control beside it no longer opens the navigation column over that control. Focus still opens it at once. - The scroll edge fade on a table or tab strip takes the foreground token, so it lightens a dark card as it darkens a light one. In the dark theme it could not be seen.
Paginationbuilt from links draws a step with nowhere to go as a disabled button, as the callback pager already did, instead of a link with no target that assistive technology could not name.AccountMenugives its trigger an accessible name, the person's name and the menu's. Below thesmbreakpoint it shows only the avatar, which is hidden from assistive technology, so it had none.Tablefades a row's hover ring in with its tint, on the fast duration and the move curve. The ring appeared ahead of the tint.
0.44.0 2026-10-28
Added
AlertandBannertakeannounce, which reads a message out when it appears, and every change to it after, without clearing it. Pass it for a result that stays on screen, such as a confirmation that replaces a form or a status a poll updates.
Changed
- No component carries
role="status"any longer.Toaster,SortableList,CopyButton,CopyField,FormMessageon its success tone, andAlertandBannerthat report an outcome are each a polite region witharia-live="polite"and no role. Chrome and Firefox expose a status role as a status bar, and the Orca screen reader speaks nothing inserted into one. A test that finds one of these regions by its role finds it byaria-livenow. AlertandBannerthat neither clear themselves, takeannounce, nor report a failure or a warning state a standing condition and are no live region, so a page that shows one, on load or after a client-side navigation, reads nothing out. A failure and a warning keeprole="alert".- The text of an outcome arrives two frames after its region mounts, in
FormMessage(one frame before) and in anAlertorBannerthat clears itself or takesannounce, and the region is atomic, as the status role made it. A page that renders an outcome inside{#if form}with its text already in it is then still a change a screen reader reads out. A consumer test that reads that text right after render waits for it withfindByText.
Fixed
Toaster,SortableList,CopyButtonandCopyFieldare heard in the Orca screen reader in Chrome and Firefox.SortableListandCopyFieldannounce through adiv, which Orca reads even when other events from the page come just before it. A paragraph lost the reorder sentence in Firefox that way.SortableListspeaks a move made with the keyboard or the handle's menu two frames after the handle takes the focus back. Firefox reports that focus as new, and Orca dropped a sentence that arrived just before it.FormMessageis heard in Firefox. Its region was filled on the first frame, and Firefox saw it arrive already full.Tabsscrolls only its own strip to show the active tab. It usedscrollIntoView, which also moved the page, so a strip placed low on a long page pulled the page down to it as soon as it mounted.Logodraws the bottom row of the mark. Its two lowest tips sat one unit past the view box and were cut off.
0.43.0 2026-10-25
Added
SortableListputs rows in order by dragging a row's handle or by pressing Alt with the up or down arrow. A mouse lifts a row after a few pixels of travel. A finger lifts it after resting a quarter of a second on the handle, so a swipe that starts there still scrolls the page. A line marks where the row will land, the box that scrolls the list scrolls near its edge, and Escape or a second finger puts the row back. Every move, and every move that cannot happen, is announced in the row's own words: "Hero moved to position 2 of 5."canMoverefuses a move by returning the sentence to say instead, and the line turns to the danger tone over a refused place. The handle also opens a menu with Move up and Move down, from a click, a tap, Enter or an arrow, so a row moves without a drag or a chord. Alt with an arrow inside a text field stays with the field, where Option and an arrow move the caret on macOS, and so does Alt with an arrow that a control in the row takes for itself.ColorFieldtakes a color typed as hex, such as#2F6FEB,2f6febor the short#26E, and stores six upper-case digits after a#. Text that reads as no color is refused when the reader leaves the field. Withswatchesit is a radio group whose value is the picked swatch's value, such as a theme token's name. A stored value that no swatch carries any longer is kept, named and sent with the form until the reader picks another. It never draws the browser's color input.normalizeHexis exported for a host that reads hex itself.DiffViewlists the changes between two versions, each labeled Added, Removed, Changed or Moved in words, and sets a changed value's old and new text one above the other. A value longer than 400 characters shows its first 400 and a Show all button, and the cut falls between characters a reader sees as whole. Each Show all names its value to a screen reader, and every value folds again when the caller passes other changes. An emptied value reads "Empty". Every label and value takes its direction from its own text. With no changes it says the versions are the same.
0.42.2 2026-10-23
Fixed
PluginCartwithper="once"prices each plugin card as "$3.00 once". The cards read "$3.00/mo" and promised a renewal that a one-time payment never charges. Monthly and annual carts keep "/mo" on the cards.
0.42.1 2026-10-23
Fixed
Buttonwith anhrefcalls itsonclick. The link dropped the handler, so a host could not handle the click in place, such as choosing an item without leaving the page.PluginCarttakesper, what the total is charged per:'monthly','annual'or'once'. It defaults toperiod. A one-time payment is quoted monthly and charged once, so its total read "$6.00/mo" and promised a renewal. Withper="once"it reads "$6.00 once".PluginCart's review bar on a narrow screen reads "2 picked · $6.00", spaced and in the body face, and its link reads "Review order".
Changed
PluginCartsets every price in the body face with tabular numerals instead of the monospace face, so the page keeps one typeface.
0.42.0 2026-10-22
Added
PluginCarttakes asummarysnippet that draws the host's own summary in place of everything inside its aside: the title line,error, the list, the ladder, the total,noticeandactions. It suits a host that prices more than the selection, such as a plan change or a discount code. The snippet receives the selection, the server total and aremovefunction that takes one selected capability out and reports it throughonchange, as unticking its card does, and leaves a capability that is not selected alone. The total is null while a price is in flight and before one is asked for, so the host tracks its own request. The aside keeps its id, its stickiness and the bar that jumps to it on a narrow screen.PluginCarttakes aheading, the panel's name: the accessible name of its aside, also printed atop the default summary. It stays "Your selection" when unset, and the default markup is unchanged.
0.41.1 2026-10-22
Changed
- The package is developed, tested and built on Node 26.12.1 with pnpm 11.25.0 only. It still supports Node 24 or later and any package manager for the people who install it, and its built output is unchanged.
Security
- The development tools moved past every advisory pnpm audit reported, vitest and Vite among them. None of them ships in the package.
0.41.0 2026-10-18
Added
SplitPanedocks a column to the start or end of a page. Above its breakpoint the inner edge drags, and the handle is a focusable separator that reports its width and moves by 24px on an arrow key, 80px with Shift, and to the smallest and largest width on Home and End. In a right-to-left page the edge follows the arrow the way it looks. A lock fixes the width, a collapsible pane folds to a strip carrying its name, and below the breakpoint it stacks in the page, hides, or floats over the page as an overlay. Collapsing or expanding the pane from its own button moves focus to the button that undoes it. The width and the lock are kept in local storage under the key the page names, as{"width": <px>, "locked": <bool>}. Storage that is missing or throws leaves the pane at its default width for the session, and the server renders the default width. A stored width wider than the screen allows is capped so the main column keeps the pane's minimum, and is written again only when the reader moves the edge.RefusalNoticedraws a write the server refused as a warningAlert: the title and sentence the page passes, an optional way forward, and a link offered on every refusal. The way forward is a secondary button and the other link a ghost, or the way forward is the primary withlead. A way forward on another origin opens in a new tab.GateNoticedraws what a page shows in place of a read that was refused or failed, for each state of the exportedGatetype: absent, locked, full, forbidden, busy, unavailable and error. Every sentence comes from thewordsprop, the locked state renders the page'slockedsnippet with the link the gate carried, and busy, unavailable and error offer a link toretryHrefthat asks for a fresh document. A read that answered draws nothing.
Fixed
FileInputnames the accepted file types in a short line. The drop zone printed itsacceptlist as written, so.csv,.json,application/jsonran as one long line of extensions and media types. Each entry now folds to the name a reader knows the format by, duplicates collapse, and the names join with "or", so that list reads "CSV or JSON". The input still receives the raw list, and a vendor type with no short form stays as written.- The package lists its stylesheets as side effects. It declared itself free of side effects, so webpack dropped a bare import of the stylesheet and the theme tokens never reached the page. The JavaScript modules still tree shake.
0.40.0 2026-10-16
Added
Paneltakestone="danger"for a group of destructive actions, such as deleting an account. It draws an opaque danger-tinted box with a danger border and puts the heading and its icon in the danger color. It is not a live region, so it can hold a form, whichAlertandBannercannot. Withvariant="plain"there is no box and only the heading carries the tone. The defaulttone="neutral"renders as before.
Fixed
- The notes on
Field'sgroupprop and onSegmentedControl'slabeledByandlabelHiddensay what the components do. ASegmentedControlnamed by aFieldcaption takeslabelHiddenas well, or it draws its own caption under that one.FilterChipstakes nolabeledBy: it names its group from its ownlabel, so give that label the caption's words. Thegroupnote had namedFilterChipsamong the controls that takelabeledBy, so a caller who followed it got a type error. Nothing renders differently. - The notes for 0.39.0 left out one case of the generated id. An
Input,Textarea,NumberInputorSearchInputgiven neither a label nor anidhas carried a generated id since 0.39.0, where 0.38.5 gave it none, so a test that expected no id on such a field now finds one. AnInput,TextareaorNumberInputamong them has also pointedaria-describedbyat its hint or error since that release.SearchInputtakes no hint or error. - A page transition no longer cross-fades for a reader who asked for reduced motion. The View Transitions API animates pseudo-elements that the stylesheet's reduced-motion rule did not match, so a page using
document.startViewTransitionor@view-transitionkept its full fade. Those pseudo-elements now finish in 1ms like every other animation, in a rule of their own, so a browser without view transitions keeps the element rule.
0.39.1 2026-10-15
Fixed
- The property that names a control after its
Fieldcaption is now spelledlabeledBy, in US English like every other name in the kit, onSegmentedControland in the wiring aFieldgivengrouphands its control. 0.39.0 spelled it with a double l. Rename it where you pass it or read it.
0.39.0 2026-10-15
Added
- A
Fieldgivengroupdraws its caption as text and hands its id to the control aslabelledBy, and aSegmentedControlgivenlabelledBytakes its name from that caption. AFieldcaption used to be a label pointing at the group, which a label cannot reach, so a click on it did nothing. The caption looks as it did, and aFieldwithoutgroupis unchanged. <!-- us-english: keep: the name 0.39.0 shipped -->
Changed
- An
Input,Textarea,NumberInput,SearchInputorSelectgiven noidnow takes a unique generated id instead of one made from its label. Two fields with one label on a page no longer share an id, so each label names its own control and each control reads its own hint or error. A page that pointed aLabel'sforor a selector at the label-made id now passesiditself.
Fixed
- A list or a calendar opened in a container too short for it on either side, such as a one-field
Modal, opens at its own height below its field. The container grows to hold it, or scrolls down to it when it cannot grow. ADatePickerthere opened 34px tall and showed only its month row, and aSelect,MultiSelect,AutocompleteorDropdownshowed no whole row. A list now keeps at least four rows in view, or all of them when it has fewer, and a calendar keeps its whole grid. - Under a finger, each
NumberInputstepper's 44px target meets the number and grows outward, in either text direction. Centered on the 32px stepper, it reached 6px into the number, so a tap just inside the field stepped the value. A mouse sees it as it was. - Under a finger, the buttons in a
CopyFieldsit 17px apart, so the 44px targets of Show and Copy meet instead of overlapping by 15px, and a tap on the end of Show no longer copies the value. A mouse keeps the 2px spacing. - Under a finger, the logo link at the top of an
AuthShelltakes a 44px target. It was 36px tall. A mouse sees it where it was. - Under a finger, the remove crosses on the top and bottom rows of
MultiSelectchips keep their 44px targets inside the field. They reached 5px past its top and bottom borders, so a tap just above or below the field removed a value. The field pads its chips 11px from its border there, and one row of chips still fills the 44px control height. A mouse sees it as it was. - The kit draws its own icons on the five-size ladder the consoles use. The show and hide eye in a
PasswordInputand aCopyField, and the copy icon beside it, are 14px to match the field's text, and were 15px. Their buttons keep their size, so no touch target moves. TheAppShellsidebar toggle is 20px, as the menu button it replaces on a phone is, and was 18px.
0.38.5 2026-10-15
Fixed
- Under a finger, the remove cross on each
MultiSelectchip and on a removableTagkeeps its 44px target off its neighbors. The chips in aMultiSelectsit 8px apart there and their rows 24px apart, and a removableTagholds that room around itself, so a tap meant to open the list or to reach the next tag no longer removes a value. EveryTagcenters itself in its row, so a plain one beside a removable one neither stretches nor sits higher. A mouse sees both as they were. - A
MultiSelectis drawn in the order Tab reaches it: the field first, then the remove button of each chip from the start of the row. The field used to be drawn after the chips while Tab reached it before them. Its chevron now sits centered at the end of the field however many rows the chips take, and a long placeholder is cut short rather than wrapped. - A list or a calendar wider than the room it opens in shrinks to that room. Under a finger the
DatePickercalendar is 320px, so inside aModalon a 320px phone it ran past the modal body, which then scrolled sideways. Where there is room it keeps its width. In a calendar shrunk that way, each day's target takes the width of its column and keeps its 44px height, so neighboring days' targets still meet without overlapping. A calendar never shrinks below the width of its days, 262px or 250px under a finger, so in a narrower container it keeps its start edge in view instead of scrolling sideways inside itself. Under a finger the previous and next month buttons keep their 44px targets inside the calendar, which used to pan about 3px sideways around them. - A
TreeViewbranch unmounts its rows once its collapse has played. A branch that had been opened once kept its rows mounted for good, so a large tree that a reader opened and shut held every row it had ever shown. - In a right-to-left page, a
Drawerslides in from the edge it is docked on and back out, as theAppShellnavigation drawer does. A drawer docked on the left slid in from the right across the whole screen.
0.38.4 2026-10-15
Fixed
- Under a finger, the options of a
CheckboxGroupor aRadioGroupsit far enough apart that each takes a 44px target of its own, in a column and in a row that wraps, so a tap near the bottom of one option no longer picks the option below it. Card options keep their spacing, and a mouse sees the list as it was. - Under a finger, every day in a
DatePickerorDateTimePickercalendar takes a 44px target of its own. The calendar opens 48px wider there, 320px, which a phone of that width holds edge to edge, and its weeks sit 12px apart, so a tap near the edge of a day no longer picks the day beside or below it. Any list or calendar that would run past the side of the screen, or of the dialog or scrolling box it opens in, now slides back inside it. A mouse sees the calendar as it was. - Under a finger, every option in an open
Select,MultiSelectorAutocompletelist is 44px tall, as aDropdownmenu item already was. The rows were 36px. A mouse sees the list as it was. - Under a finger, every
TreeViewrow is 44px tall, the height aSidebarNavrow takes there. The rows were 32px. A mouse sees the tree as it was. - Under a finger, the close cross on a dialog opened with
openDialogtakes a 44px target, as the crosses onModalandDrawerdo. It was 32px. The cross is drawn the same size, and a mouse sees it as it was. - Under a finger, the cross that clears an
Autocompletetakes a 44px target, as the one in aSearchInputdoes. It was 24px. A mouse sees it as it was. - A
FileInputreached with the keyboard draws the 2px brand focus ring around its drop target. Focus used to land on the hidden input and show nothing at all. A click still opens the file picker with no ring. - A
NumberInput, aMultiSelectand the search box inside an openSelectorMultiSelectlist draw the 2px brand focus ring under the keyboard, asInputandTextareado. Focus used to show only as a 1px change in the border color. Avatarinitials read at 4.5:1 or better on every one of its eight colors in both themes. Seven of the eight read lower in one theme or the other, down to 1.87:1 in light, because dark initials sat on a see-through color that took on whatever the avatar was drawn over. The five strong colors are now solid behind the same initials and the three soft ones a light tint behind body-colored initials, and each name keeps its color.- The count on the active
Tabstab reads at 4.5:1 or better in both themes, at least 5.42:1. Its tint was four times as strong as every other brand chip's, and the count read as low as 3.28:1. It now takes the same light tint as the rest. - A colored
SidebarNavbadge and the colored initials in anAccountMenuread at 4.5:1 or better on every row, at least 4.68:1. On the current page's row, and on any row under the pointer or a finger, the row's own highlight showed through the badge and a violet badge read as low as 3.69:1. A badge on a resting row looks as it did. - The warning sign in a
confirmdialog is sized by its icon size, 20px, rather than by a class. It looks as it did. - On a phone, the
AppShellnavigation drawer slides in from the edge it is docked on and back out, the right edge in a right-to-left page, and the dimmed page behind it fades in and out, as aDrawerdoes. Both used to appear and vanish in a single frame. - A
Bannerthat is dismissed, by hand or byautoDismiss, fades away as anAlertdoes. It used to vanish in a single frame. - A screen reader announces a
dangerorwarnBannerat once, as it does anAlertof the same tone, and waits for a pause for every other tone. Every banner used to wait, so "Payment failed" came after whatever was being read. A banner withautoDismissstill waits, whatever its tone. - A
TreeViewbranch slides open and shut with its chevron, the way anAccordionpanel does. Its rows used to appear and vanish in a single frame. A branch nobody has opened still renders nothing under it, so a large tree costs no more than it did. - A screen reader meets a
MultiSelectas one field that reads its label and then the chosen values, followed by a remove button for each value, and Tab reaches the field before any remove button. The remove buttons used to sit inside the field's own control, a control nested in a control. After a value is removed, focus returns to the field, and a disabled field removes nothing and leaves its remove buttons out of the Tab order. A press anywhere on the field still opens the list, and the field looks as it did. - Under a finger, the check box in a checkable
TreeViewrow takes a 44px square of its own beside the label, so a tap that means to check a row no longer opens or selects it instead. The rest of the row still selects it, and a mouse sees the tree as it was. - A
dangerorwarnAlertgivenautoDismisswaits for a pause before a screen reader announces it, as every other tone does. It used to interrupt the reader and then clear itself, leaving nothing to act on. WithoutautoDismissadangerorwarnalert still interrupts.
0.38.3 2026-10-14
Fixed
- Under a finger, each
TimePickersegment takes a 44px target of its own, the hour, the minute, the second and AM or PM alike. The segments spread a few pixels apart there so no target takes its neighbor's, and a press anywhere else in the field, on its padding or a colon, focuses the nearest segment, as a native time input does, under a mouse as well. A mouse sees the field as it was, and the keys, the names a screen reader reads and the value are unchanged.
0.38.2 2026-10-14
Fixed
- Under a finger,
FilterChipsthat wrap to a second row keep a 44px target on every chip. The rows sit 18px apart, so a tap near the bottom of a chip no longer lands on the chip below it. A mouse keeps the 6px gap. - Under a finger, each
SegmentedControlsegment takes a 44px target, at least 44px wide for a short label such as "6h" and 44px tall inside a row that scrolls sideways. The track draws its frame as an inset outline with no border there, so the scroll box no longer cuts a pixel off the top and the bottom of every target, and a contrast theme keeps the frame. A mouse keeps the border and the widths it had. - Under a finger,
Paginationpage numbers sit 16px apart, so each one takes a 44px target of its own instead of 30px. On a phone held by a finger the pager shows the first page, the current page and the last, with a gap mark for each run between them, so it stays on one row. From thesmwidth up, and under a mouse, it shows the pages it did before.
0.38.1 2026-10-14
Fixed
- A
PageShelltitle with no spaces, such as a tenant slug, breaks inside the page width. It used to run past the right edge and widen the page, so on a phone the whole page panned sideways. A title with spaces still wraps at them. - A long
FileInputaccept list wraps inside the drop target. A list with no spaces used to print as one line that spilled out of both sides of the dashed box. The input'sacceptattribute is unchanged.
0.38.0 2026-10-14
Added
SegmentedControlandFilterChipstakeid,hint,erroranddescribedBy, the field propsInput,SelectandRadioGrouptake. The hint or the error shows under the row, and the group'saria-describedbypoints at it and then at the ids indescribedBy, so a row inside aFieldreads that field's message. A segmented row of radios carriesaria-invalidwhile it shows an error. Both link modes take the same props, and a row given none of them looks and reads as it did.- Both rows now take a number from Svelte's id counter even when no new prop is set, so a generated id rendered after a
SegmentedControlor aFilterChipson the same page is renumbered. Input,SelectandRadioGrouptakedescribedBy, ids read after the control's own hint or error, andRadioGrouptakesidfor its fieldset. A control given neither looks and reads as it did.CopyButtontakestitle, a tooltip shown on hover and on keyboard focus. The button keeps its name, its visibletextor itslabelon an icon alone, and a title that says more than that name becomes its accessible description.CopyFieldtakeswrap, which shows the whole value across as many lines as it takes instead of one truncated line. It breaks anywhere, since a key or a token has no spaces to break at. A masked secret stays on one line and wraps once revealed. The default is still one truncated line.EmptyStatetakeslevel,2or3, the heading level of its title, named asSectionHeadingnames it. It stays an h3 by default. A page whose only other heading is its h1 passes2, so a screen reader does not meet a jump from h1 to h3.
0.37.0 2026-10-13
Added
FilterChipstakes anhrefon each chip, the link modeSegmentedControlalready has. Every chip carries one or none does, the current chip is markedaria-current="page", and a link row works before the page hydrates, so a filter held in the URL can offer more options than a segmented control holds.CopyStateandcopyToClipboardare exported, with theCopyStatustype, so an app builds its own copy control on the kit's clipboard write and its confirmation timing.CopyButtontakestext, a visible label beside its icon, which also becomes its accessible name.setDocumentTitleFormatlets an app state its browser tab title format once, in its root layout. EveryPageShellbelow it then titles the tab from itstitle, or fromdocumentTitlewhen the tab should read differently. With no format stated,PageShellrenders no title element, so an app that renders its own keeps it.AuthShelltakes alockupsnippet in place of the product mark, and passesclassandstyleto its outer element, so an app can show a tenant's logo and scope a tenant's accent to the sign-in frame.- The
StatusToneandAccentTonetypes are exported.
Fixed
- Under a finger, every
SegmentedControlsegment and bothNumberInputsteppers take a 44px target, and a small segmented track grows to the control height. A number field keeps 64px for its value inside aw-32. Nothing moves under a mouse. - The collapsed
SidebarNavrail stacks a group row's disclosure under its link. Side by side the two did not fit the 56px rail. Under a mouse the link shrank to 18px with its icon under the chevron and the rail scrolled sideways, and under a finger it scrolled further.
0.36.0 2026-10-12
Added
Buttontakessize="icon", a square for a button whose only content is an icon. The icon sets the size and the box adds 8px round it, and a coarse pointer grows it to the control height like every button. Name it witharia-label, which also becomes its hint on hover.
Changed
ThemeToggledraws itself as a ghostButtonat the icon size, so a control set beside it takes the same box by naming the size rather than copying its classes, and its name shows on hover as well as to a screen reader.
0.35.2 2026-10-12
Fixed
Autocompleteposts the chosen option's value. Given a name, it put the name on the visible input, which holds the option's label or the words being typed, so a form posted "Alpha" where the option was "a". The name now goes on a hidden input that holds the value, the waySelectposts.- The motion presets keep an element's own transform. Under
animate:motion.reorder, a leaving list item is held where it stood by an inline translate, and an exit that animatedtransformreplaced it, so the item jumped to the corner of the list as it faded. Every frame now draws the element's inline transform first, and<li transition:motion.toast animate:motion.reorder>leaves from its place.
0.35.1 2026-10-07
Fixed
Checkboxkeeps a tick made before the page hydrates. The box wrote itscheckedprop over the input as the page hydrated, so a box ticked while the page was still loading came back empty, and a form waiting on it stayed disabled. It now bindscheckedtwo ways and adopts the input's live state.
0.35.0 2026-10-06
Added
FilterChips, one filter out of many: a wrapping row of pressable chips, each with an optional count and icon, one always pressed. For a filter with more options than aSegmentedControlholds.
Changed
PluginCartdraws its category row withFilterChips.
0.34.0 2026-10-06
Changed
- Breaking:
Selectalways draws the kit's listbox. The native select is gone, because the browser draws its option list in its own font and colors, which no theme reaches.mode,children(hand-written option elements),onchangeand theSelectChangeEventtype are removed. Passoptionsas data and read the value throughbind:valueoronvaluechange, which now fires once the hidden input holds the new value, so a form can be submitted from it. TimePickerdraws its AM and PM part as a spinbutton like its other segments. A click, an arrow key, or A or P sets it.
0.33.1 2026-10-03
Fixed
PluginCartwraps a long one-word plugin name beside its price instead of running it under the price.
0.33.0 2026-10-02
Added
PluginCarttakesstickyTop, how far below the top of the window the summary sticks, so a page with a sticky header keeps it in sight.CartPlugin.maturity, the catalog's maturity. A card shows a Beta badge when it is "beta". Until a catalog sends it,betaOfreads a "Beta." sentence or a "Beta:" caveat from the blurb.
Changed
PluginCartcards put the price beside the name, the category and a Beta badge on a row of their own and the description alone below, and every card in a row matches the tallest. A screen reader hears the price, the description and the badges with each card.- Categories are wrapping chips with their counts, in place of a segmented control that scrolled sideways and hid the last categories.
- Quick picks are labeled "Start from" and drawn as buttons. The summary draws the volume ladder as a meter, and on a narrow screen a bar under the cards keeps the count and the total in view and links to the summary.
0.32.1 2026-10-01
Fixed
AppShellpositions its scrolling main region. Ansr-onlylabel low on a long page took the viewport as its containing block, escaped the region's clip and stretched the document past the shell, which gave the window a second scrollbar and scrolled the sidebar and header off screen.
0.32.0 2026-09-28
Added
PluginCart, the capability picker and its running summary, so the public price page and the customer portal stop shipping two carts. It renders a quote and never computes one: the total, the volume discount and the annual figure all arrive from the server, and a quote that no longer describes what is selected is withheld rather than shown. Its checkboxes carry a name and a value, so a host that wraps it in a form still posts the selection with no JavaScript running.ladderState,ladderRows,billedCountandformatCents, the volume ladder as a reader sees it. Presentation over the brackets, never pricing.CheckoutSteps, the same four labels on both of the sites a purchase crosses, so the hop between them stops reading as arriving somewhere else.
0.31.1 2026-09-26
Fixed
PasswordInputandSearchInputkeep what was typed or autofilled before the page hydrated. Both set the field's value one way, so hydration wrote the empty model back over it, and a required password field then stopped its form from submitting.
0.31.0 2026-09-26
Added
CopyField: a read-only value with its copy control inside the field's end, for keys, secrets, hashes and addresses.secretmasks the value and adds a reveal beside the copy;actionsputs the caller's own controls in the same row. Focusing the field selects the whole value. It replaces a read-onlyInputwith a separate outlined button beside it, which read as two controls of different heights.watchThemePreference(onChange): hears every preference any control on the page sets, and one another tab stores, so a control showing the preference can follow a change made elsewhere.
Fixed
ThemeTogglefollows a preference set by another control. A settings picker on the same page left the header toggle on its old value, and a toggle still holdingsystemrepainted the page to the OS theme at dusk over the reader's explicit choice.setThemePreferenceandsetThemenow announce the change and the toggle listens.
Changed
CopyButtonturns its icon into a check in place, crossing the two over on the motion rungs instead of swapping them in one frame, and shares its clipboard handling and timing withCopyField.- Copying works on an insecure origin. Where the async clipboard is absent or refuses, both controls fall back to the legacy copy command inside the click, so a console reached over a LAN address without TLS still copies.
0.30.0 2026-09-25
Added
InputandTextareatakemono, which sets the control alone in monospace. Afont-monoclass reaches the field wrapper, so it turned the label and the hint monospace as well.
Changed
SegmentedControlmarks the chosen segment in the brand tone with an inset ring. On the surface-2 track it read as a slightly lighter box, and in the dark theme the bold label was the only visible difference.
Fixed
Dropdownsizes its panel to its longest item, capped atmax-w-xs, and items no longer wrap. The panel was positioned against the trigger's wrapper and shrank to its width, so a label past nine characters broke onto two lines.
0.29.0 2026-09-24
Changed
Logodraws the brand book lockup: a bold wordmark with the E in the brand accent, and the mark's faces in the brand book's colors for each theme, the same lockup the documentation and marketing sites ship. The mark used to borrow the text ramp, which darkens for contrast in the light theme and put the top face darker than the front.- The mark paints from its own tokens:
--color-mark-ink,-foot,-step,-top,-side,-faceand-accent. A host that overrides--color-brandno longer recolors the mark; override the--color-mark-*tokens instead. - The wordmark is announced once as "LyEve" rather than in fragments.
Fixed
- Under forced colors the mark's neutral faces take
CanvasTextandGrayText, so the column no longer disappears on a high-contrast canvas. - Printing keeps the light-theme faces, so the column is not white on paper.
0.28.0 2026-09-23
Added
BannertakesautoDismiss, on the same terms asAlert: a confirmation clears itself after five seconds and carries a close button, the timer holds while somebody is reading it, and a warning or a failure stays until dismissed. Which of the two components a surface reached for decided whether its confirmation ever left the screen.
0.27.0 2026-09-23
Added
AlerttakesautoDismiss. A confirmation clears itself after five seconds and carries a close button, so nobody has to wait for it. The timer holds while the pointer is over the alert or focus is inside it. A warning and a failure are unaffected: they stay until dismissed, because the reader has to act on them.
Changed
Alertpicks its live region from its tone. A failure and a warning takerole="alert"and interrupt; a confirmation, a neutral note and a brand note takerole="status"and wait for a pause. An assertive region that then removes itself is the worst of both.AppShelleases the sidebar between the rail and the expanded column, and eases the rail's elevation with it. Both changed in a single frame before.
Fixed
- An icon inside a
Badgetook a line of its own, so a pill carrying a count and a glyph rendered two rows tall. Preflight makes every svg a block and the badge's label is a block, which is what gives it its ellipsis; the icon is inline again and sits on the text's optical center. Togglecarries thedata-fieldmarker. Without it the fit that sizes an overlay to the form inside it could not see a toggle, and six drawers in the consuming applications opened a rung too narrow. The suite that checks for the marker now finds a control that is its own label, which is the shape it was missing.
0.26.0 2026-09-22
Added
- One width ladder for every surface lifted off the page, and a
sizethat measures instead of guessing.Modal,Drawerand the dialog stack kept three ladders and none agreed, so the same form read at 672px, 384px or 512px depending on which surface a page opened it in. The five rungs are--container-overlay-sm|md|lg|xl|full(448, 576, 704, 896, 1088), each wider than the widest rung it replaces.ModalandDrawerdefault tosize="auto", which takesmdup to four fields,lgpast four andxlpast eight; the count is read from thedata-fieldmarker every labeled control now carries, so a radio group counts once and a form that reveals fields while it is open is re-measured. A caller that names a rung keeps it. AuthShell, the frame for a page the app shell does not wrap: sign in, sign up, a password reset, an invitation, first-run setup. A full-height main on the ink token, a centered column atmd(a form) orlg(a walkthrough), the lockup, oneh1at one size, an optional description, the children on a card, an optionalactionsrow above (a theme toggle) and an optionalfooterline under the card (the one link off the page). Every console hand-wrote this frame and no two agreed: three heading sizes, a lockup on some pages, a theme control on others, and one class that named no token at all.AppShellstates the signed-in frame once; this states the signed-out one.
Changed
- The page caps grew and became tokens:
--container-page-narrow896px,--container-page-default1152px and--container-page-wide1536px, up from Tailwind's 768, 1024 and 1280.PageShellreads them by name, so a console retunes the cap for every page at once rather than per page.
0.25.1 2026-09-15
Fixed
- An overlay's exit now plays when the page removes the component, not only when the component closes itself. The presets were local Svelte transitions, which play only when their own block toggles, and every console wraps a
Modalin a page-level{#if}so the form inside starts fresh each time; on those pages the dialog still snapped shut. Every preset directive in the kit is|globalnow, and the consistency suite refuses one that is not. A page's own surface on a preset needs the same modifier:transition:motion.popover|global.
0.25.0 2026-09-15
Added
AppShelltakesheaderHidden, bindable, which puts the header bar away above md: for a page that owns the viewport, such as an editor in a focus mode. Below md: the bar stays whatever the prop says, because it carries the hamburger and that is the only way into the drawer. A page that hides the bar keeps its own way back on screen, since the shell's controls go with it.PageShelltakestitleHidden, which renders no title row at all and keeps the page's one h1 assr-only. An editor whose toolbar is its header spent two rows before its canvas, the title row and then its own toolbar carrying the same name, with the shell's gap between them. A heading query and a screen reader still find exactly one h1;backrenders nothing on such a page, so the page takes on the way back itself.SegmentedControltakeshrefper option. When every option carries one, each segment renders as a plain anchor with no click handler, the chosen segment carriesaria-current="page", and the group is agrouprather than aradiogroup, so a page can keep its choice in the URL, survive a reload and work before any script has loaded. The arrows still move between segments, and the tab stop follows focus inside the group and returns to the current segment when focus leaves it. A row that mixes links and radios is refused with an error, since a radio beside two links would report a choice the URL never learns about. The value mode is unchanged.- One motion vocabulary, declared in
styles.cssbeside the palette: four durations (--duration-fast,base,slow,progress) and three curves (--ease-enter,exit,move), each named for what it is used for. Thetransition-*utilities now default to the fast rung and the move curve, sotransition-colorson its own is complete, andduration-base,duration-slow,duration-progress,ease-enter,ease-exitandease-moveare utilities for the rest. The values were never stated in one place before: three entrances ran at 120, 140 and 150ms on Tailwind'sease-out, and fifty class attributes saidduration-150to mean the default. motion, a namespace of Svelte transitions that read those tokens at run time:dialog,scrim,drawer,popover,toastfor a surface that mounts and unmounts, andreorderforanimate:on a keyed list. An exit runs one rung faster than its entrance, on the exit curve. Every preset plays nothing for a reader who has asked for reduced motion, and nothing in a document with no Web Animations API, so a test that mounts and unmounts a surface sees what it always saw.AppShellnarrows the sidebar to a 56px icon rail between md: and lg:. At 768px the 224px column left the page 496px, and a flow editor with two docked panes had no canvas at all;--spacing-nav-railexisted and nothing used it. Under a pointer or with focus inside it the rail opens to the full column over the page, at the dropdown layer, and closes when either leaves, so the labels are one hover or one Tab away and the page keeps its width. Thebrand,navandsidebarFootersnippets are told{ rail }, so aSidebarNavtakes it ascollapsedand a brand row can drop its wordmark; a snippet written for the old contract still renders, clipped to the rail. A page can ask for the rail at every width above md: withrail, which is the focus mode the flow editor wanted; the drawer below md: is unchanged, and a collapsible shell that is collapsed still puts the sidebar away.SidebarStateis exported for a caller that types the snippet by hand.- Every interactive primitive meets a finger at 44px and a mouse at the size it always had. Measured over 48 routes at 400px, 2,007 of 2,097 visible controls were under 44px and the kit owned 96% of them, because the control height is 38px and it is right at 38px on a desktop. The fix keys on
pointer: coarse, so the desktop is unchanged to the pixel, and works two ways.--spacing-controlis 44px under a coarse pointer, and the controls whose visual may grow (Buttonat every size,Tabs,SidebarNavrows and disclosures,ThemeToggle,AccountMenu,Collapsible,Dropdownitems) statecoarse:min-h-control, so a button and the input beside it grow together and stay level. The controls that are small on purpose keep their visual and grow an invisible hit box centered on themselves through the newhit-areautility:Toggle,CheckboxandRadio(on the label, which reaches the input),Breadcrumblinks,PageShell's back link,Pagination,CopyButton, the close and dismiss crosses ofModal,Drawer,Alert,Banner,ToasterandTag, the clear and reveal buttons insideSearchInputandPasswordInput,MultiSelect's chip remove, andDatePicker's days and month steps. Thecoarse:variant and the utility are declared in the theme file beside the tokens, so a consumer's build emits them and can use them on its own controls.
Changed
- Every overlay now leaves the way it arrived.
Modal,Drawer, the dialog stack and every toast played a CSS entrance and then vanished the instant their{#if}turned false, which read as a fault after the easing in.Dropdown,Select,MultiSelect,AutocompleteandDatePickerpanels, which appeared with no motion at all, now unfold from the edge they hang off;Tooltipfades and settles; theAccountMenupanel plays the same frame on the way in. ASidebarNavgroup collapses its row to nothing the way anAccordionItemdoes instead of toggling display, and stays in the document, inert, so the disclosure'saria-controlskeeps its target. - The dialog stack no longer waits a hand-written 200ms
setTimeoutbefore removing an entry: removing the entry plays the exit.
Fixed
- A
DatePicker's calendar, and so aDateTimePicker's, is no longer cut off inside a modal. The calendar sat under its trigger unconditionally, so a picker near the bottom of a modal body opened into the part of the scroll region nobody could reach and was cropped below the weekday row. The calendar is now the shared panel surface, placed the way every listbox panel is: it opens upward when the room below inside the nearest clipping ancestor is short and the room above is larger, it is re-measured on resize and on scroll, and inside a container too short for it on either side it scrolls rather than being cut. Its width moved from an arbitrary value onto the spacing scale; the calendar itself is unchanged. BadgeandButtonlabels never break inside a word. Neither carriedwhitespace-nowrap, so under aTable's defaultoverflow-wrap: anywherea status badge rendered asdra ft, a role assup er_a dmi n, and a two-word ghost button in a flex row broke across two lines; one console marked 156 cellsdata-cell="nowrap"to stop it. ABadgealso caps at its container's width and truncates with an ellipsis when its caller sets a narrower one; its label sits in a span of its own for that, becausetext-overflowdoes not reach into a flex item. AButtonwhose width is capped wraps its label in atruncatespan itself, since the label and an icon before it are flex items and the button cannot tell them apart.Tablehead cells align to the start. The table saidtext-startand the browser's ownth { text-align: center }beat the inherited value, so every heading floated over a left-aligned column unless the page wrotetext-lefton each one. A column markeddata-col="numeric"on its head and body cells ranges to the end in tabular figures instead. A sort button the page puts in a heading gets the same 44px hit box the kit's own small controls grow under a coarse pointer, through the table.Tabsscroll sideways when the strip does not fit, and say so. Five tabs are 480px, and at 400px the fifth was past the edge with nothing on screen to say it existed; the workaround was a wrapped strip, which breaks the one line the underline runs along. The strip is a scroll box now, with the edge fadesTablealready draws on the side the tabs continue past, no scrollbar of its own, and the active tab scrolled into view when it changes. A tab never breaks inside its label, its focus ring is inset where the box cannot clip it, and the count pill is 12px, the last text on a measured page under that floor. The caller'sclasslands on the frame around the strip, so a margin does not scroll with it.PageHeaderandSectionHeadingaction slots wrap. Both wereshrink-0, which sizes a flex item to its content and so cannot wrap even when the page puts a wrapping row inside it: four actions pushedNew flow83px past a 400px screen, and five filter chips ran 212px past it. The slot now sits beside the title while both fit, drops under it at the end edge when they do not, and wider than the row on its own wraps its controls inside itself with every line ending at the end edge, in the order given.
Removed
--duration-modal-in,--duration-toast-in,--duration-drawer-inand--duration-collapse, with themodal-in,drawer-in-*andtoast-inkeyframes. Nothing outside the kit read them; the presets replace them.
0.24.0 2026-09-15
Added
PageShelltakesback, an{ href, label }pair rendered above the title as an arrow and the parent page's name. A page two levels into a console had only the sidebar and the browser to get back with; a third of the nested pages in one app built a breadcrumb of their own, half of those drew an arrow by hand, and the rest offered nothing. The link is a plain anchor in the muted token, foreground on hover, with the kit's focus ring, and it shares the breadcrumb's row when both are given, the back link first, so a page with both does not stack two lines of navigation over its title.PageHeaderis unchanged.
Fixed
- A
Toggle's off track reads as a control on a raised panel. The track was painted in surface-2 with no edge, and a form that sits on a surface-2 panel painted the panel the same shade, so until the switch was on only the knob showed, as a lone dot beside its label. The track now carries the same resting border every other control draws, which clears 3:1 against both surface levels in both themes; on, the border takes the fill's color, so the on state, the disabled state and the focus ring look as they did, and the knob's travel is unchanged.
0.23.3 2026-09-14
Fixed
- A listbox row's active ring and hover tint stay inside the panel. The rows were square and full width inside a rounded, clipping surface, so on the first and last row the ring's corners were cut by the frame and the arc read unfinished. The list now insets its rows and each row carries a radius one step inside the surface's, so what a row paints has corners of its own; group headings and the empty line keep their text flush with the rows.
- A panel opens upward when there is no room below.
Select,MultiSelect,AutocompleteandDropdownalways hung their panel under the trigger, and nothing measured the space it needed, so a listbox near the bottom of a modal opened into the part of the modal body nobody could see without scrolling first. Each panel now measures its anchor against the nearest scrolling or clipping ancestor, or the viewport, on open, on resize and on scroll of that ancestor; it goes above only when it does not fit below and there is more room above, and its list is capped to the room on the chosen side.Dropdowntakes the shared surface and scroll cap with it, so a long menu scrolls instead of running off the window.
0.23.2 2026-09-13
Fixed
- A
CheckboxGrouporRadioGrouplegend keeps its distance from the first option. A rendered legend is not a flex item, so the fieldset's gap applied between the option list and the hint and never under the heading, and "Events" sat on its first row of boxes where anInputlabel sits 6px above its control. The legend now carries that 6px itself, the same distance aFielduses, and a hidden legend takes none. - A
Tablerow's hover ring follows the frame's rounded corner. The ring was a box-shadow on the row, which takes no radius, so on the last row it drew square inside a rounded frame and the two bottom arcs sat bare outside it. The ring is drawn on the cells now, with the last row's outer cells rounded to the frame's inner radius so the shadow goes round the corner with it.
0.23.1 2026-09-13
Fixed
- A tooltip is sized to its text and placed outside any scroller. The box was positioned inside an inline wrapper the size of its trigger, so the hint on an icon button took the button's width and broke one character per line, and a scrolling ancestor clipped it: a table's horizontal scroller clips vertically as well, so a hint on a row action was cut at the header row. The box is now fixed to the viewport and positioned from the trigger when it opens; a scroll anywhere closes it, since a fixed box cannot follow a trigger that scrolls out from under it.
0.23.0 2026-09-13
Added
- An icon-only
Buttonshows its name on hover and focus. The name was already there asaria-label, announced to a screen reader and shown to nobody else, so what a trash can or a toggle did was learned by pressing it.hinttakes a string to say something other than the name, orfalseto keep the button silent; a full-width button stays full inside its hint, and a button with visible text and noaria-labelrenders as before. Tooltiptakesdescribe, on by default. Off, the text stays visual and the trigger gets noaria-describedby, for a trigger that already carries the same words as its name; with it on, a button named Delete was announced as "Delete, Delete".
0.22.2 2026-09-13
Fixed
- A confirm dialog no longer renders the generic header above its own title. That header was an empty row holding only the close control, and the overlay put the first focus on that control, so a dialog asking "Delete?" opened with the ring on an X and Enter one Tab away from the danger button. The title now sits beside the warning icon, Cancel is the way out, and Escape still dismisses.
- Focus lands on Cancel when a confirm dialog opens, so an Enter pressed before the dialog was read does nothing.
Added
- The overlay honors
data-initial-focusbefore falling back to the first focusable element, so a panel can name where focus lands. confirm()takes adetailoption, rendered on its own monospace line under the message. It holds the identifier a message used to carry mid-sentence, where a UUID wrapped.
0.22.1 2026-09-12
Fixed
- A listbox row's active ring no longer draws past the rounded corners of the panel that holds it. The ring is square and the panel is not, so on the first or last row the corners poked out of the panel. The surface clips now, and
Tabletakes the same clip on its frame so nothing a row draws can pass the frame's corners. AppShelldrops the browser's default outline on its landing region. The region is a target for the skip link, not a control, and the outline boxed the whole page body the moment the link had been used.
Changed
- The declared Node floor is 24. Continuous integration has run on Node 24 for some time and the manifest still said 20, which described a runtime nothing was tested against. Node 22 consumers are no longer within the declared range.
0.22.0 2026-09-09
Changed
- Nothing in the package. The release carried edits to the pull request template only. The entry that stood here repeated the 0.21.0 entry below.
0.21.0 2026-09-09
Added
Paginationtakescount, the rows the page on screen actually holds, and states a range for a list with no total:jobs 51 to 100rather thanjobs, page 2. Without a total the component knew where a page started and not where it ended, because the last page is short and nothing told it how short, so an application whose endpoint reports no row count had to keep its own pager to say the sentence.countis read only whentotalis absent, and a page of no rows still names the page rather than printing an inverted range.
0.20.0 2026-09-09
Added
Paginationtakes anounand names it in the summary:files 51 to 100 of 4,210instead of51 to 100 of 4,210,jobs, page 3instead ofPage 3, andNo webhooksinstead ofNo results. A console that stacks several lists under one page title said which count belonged to which list nowhere. The word is plural at every size, because it labels the collection rather than agreeing with any figure in the sentence. A caller that passes no noun reads exactly as it did before.- The figures in that summary are grouped:
of 4,210, notof 4210. The pager carries the largest number on the screen and sat under tables that already grouped their own, so one screen printed the same kind of number two ways. Grouping isen-USrather than the host's locale, because a server and the browser that hydrates its output need not report the same one and the figure would change under the reader. The page number of an uncounted list is grouped too, sincehrefmode puts it in the URL and a link can land deep in a list nobody stepped through. The numbered page buttons stay ungrouped: they are a fixed square a separator does not fit.
0.19.0 2026-09-09
Changed
@lucide/svelteis a peer dependency instead of a dependency, at>=0.511.0 <2. It was the only runtime dependency the kit owned, and every application that consumes the kit depends on it directly too, so each resolved two copies: 42MB on disk in one of them, and eleven of the icons it imports shipped twice in that application's built bundle. Add@lucide/sveltealongside the kit if it is not already in your dependencies: a missing peer is resolved for you by pnpm and by npm, and not by yarn.
Added
- Every component lays out with logical properties, so the kit mirrors under
dir="rtl". It did not: the count was 69 physical directional utilities against 16 logical ones, and the visible end of it was a sidebar that stayed on the left in a right-to-left page. A unit gate holds the line, because the sweep is the easy half and the next component added would have started physical again. Button,Pagination,SegmentedControl,Tabs,ThemeToggleandCopyButtonacknowledge a press.hover:compiles inside@media (hover: hover), which is false on a finger, so a tap ran from rest to rest with the action already fired and the control never showed it had been touched. Every pressed step moves the same way on both ramps, so a press does not read as one gesture in one theme and its opposite in the other.TabletakescellandtruncateAt, and a single column overrides both throughdata-celland the--cell-truncatecustom property. One cell holding an unbroken token, an API key or a signed URL, pushed the table past its container and the columns after it left the screen.celldefaults towrap, which breaks that token and does nothing else.truncateholds a cell to one line and keeps the whole value in its title.Paginationrenders links instead of buttons when given anhrefbuilder, and pages fromhasNextwhen the endpoint states nototal. A callback pager cannot run before the page hydrates, so paging a server-rendered list did nothing at all until the bundle arrived, and a list endpoint that reports no count could not drive the component.page,total,perPageandonchangebehave exactly as before.- The console type scale is declared beside the brand ramp in
styles.css:--text-xsthrough--text-2xl, each with its leading. The brand ramp is a marketing ramp with no step at 14px, which is the console's body size, so the components were built from sizes the token file never mentioned while five ramp tokens were referenced by nothing. Nothing renders at a different size for this.
Fixed
ButtonandPaginationdrop an href whose scheme no component here will emit, and the rule lives in one place instead of two. The URL parser trims surrounding whitespace and strips tab, line feed and carriage return from anywhere in the value before it reads the scheme, so a tab written inside the wordjavascriptstill navigated while an anchored test on the raw string saw an unknown scheme and allowed it through.- The current row in
SidebarNavanswers the pointer like every other row. It was the only paint with no hover step, and it is the tail of the active trail, so on any nav deep enough to have sections it is a nested entry: beside siblings that all lit up, the row for the page you were on read as the one dead entry in the list.
0.18.0 2026-09-08
Added
Logorenders the canonical product mark. Every surface that needed it drew its own inline SVG, so the mark drifted between them and a change to it had no single place to land.- The theme control offers a system preference alongside light and dark.
ThemePreferenceis'light' | 'dark' | 'system', andgetThemePreference,setThemePreference,resolveTheme,systemTheme,nextThemePreferenceandwatchSystemThemeare exported with it. A visitor who has expressed no preference now follows the operating system, and keeps following it when the system flips, rather than staying pinned to whichever theme the first paint chose.getTheme,setTheme,toggleThemeandthemeBootScriptare unchanged. - Stacking order, motion and print are declared as tokens. Overlays, drawers, modals, dropdowns, tooltips, toasts and the skip link each name their layer (
z-overlay,z-drawer,z-modal,z-dropdown,z-tooltip,z-toast,z-skip-link) instead of carrying a bare number. Seven components heldz-50and one heldz-[60], so everything at one value fell back to DOM order and a tooltip could sit beneath the modal that opened it. Print is declared the same way:data-print="hide"on chrome, anddata-print="unclip"where a scroll container would otherwise crop the printed page to a single screen. Tablefades the edge that has more content to show, so a table wider than its container says so instead of ending flush at the boundary.confirmtakesconfirmLabelandcancelLabelthrough the newConfirmOptionstype. The dialog has always read them, but reaching them meant dropping to the lower level dialog API, so in practice every call site took the genericConfirmand no confirmation named the action it was confirming. A button readingDeletesays what happens; one readingConfirmsays only that something does.
Changed
PageHeaderandSectionHeadingsit on the brand type ramp. A--text-*token sets font size and nothing else, so both headings inherited whatever line height their parent happened to have. They now ask for the ramp's leading and tracking by name. The level 2 section heading stays at 16px, because the ramp holds nothing between 16px and 22px and 16px is the size of the card title it has to sit below.
Fixed
- The skip link moves focus to the main region. It scrolled there and left focus behind, so the next Tab went back into the navigation the link exists to skip.
Dropdownimplements the menu keyboard contract it declares. It took the ARIA menu role without the behavior that role promises: arrow keys, Home and End, type-ahead, and Escape returning focus to the trigger.- The icon-only controls in
Autocomplete,MultiSelectandSearchInputmeet a 24px target. The clear and remove affordances were drawn at the size of their glyph, which left them under the minimum on touch. Table's scroll container is reachable by keyboard, and rows show a hover state. A region that scrolls could be scrolled only by pointer.- The exported
VERSIONagrees withpackage.jsonagain. The 0.17.1 release bumped the manifest and the changelog and left the constant at0.17.0. The two are checked against each other before the package is built, so the released tree did not build.
0.17.1 2026-09-07
Fixed
- The package listed itself in its own
dependencies, so the lockfile resolved@lyeve-labs/ui-kitfrom the registry as a dependency of@lyeve-labs/ui-kit. A dependency bump meant for a consumer of the kit had been applied to the kit. Nothing imported it. The supply-chain exemption that had been added to let the entry install is removed with it.
0.17.0 2026-09-07
Added
AppShelltakescollapsibleand a bindablecollapsed, which put the sidebar away above md: and give the control that brings it back the hamburger's own square and gutter. Three products render this shell, and a page that owns the viewport, a canvas or a split pane, had 240px less than the viewport with no way to ask for it back. It is opt-in rather than on by default, because a new button in three headers at once is a decision each of those products makes for itself, and nothing here persists the state: where it is remembered is the app's decision, and the server cannot know, so the first paint is always the expanded one. Below md: the flag is ignored, since the same aside is the drawer there and honoring it would leave the hamburger opening nothing.
0.16.0 2026-09-06
Added
SectionHeadingtakesvariant="eyebrow", the small uppercase label the consoles head a band of content with. It is a second treatment rather than a third level, because the element is a decision about document structure and the treatment is not: an eyebrow appears at both levels. The kit never named it, so thirty of them shipped hand rolled across two apps in four different bottom margins, and two bands on the same page sat different distances from their content. The heading carries no margin at either variant - the stack around it owns that distance.
0.15.0 2026-09-06
Added
AppShell, the authed application frame: the sidebar, the header bar and the content column. Three apps each hand rolled this and no two agreed. The sidebar was 224px in the admin and 240px in the customer portal and the ops console, opaque in two of them and 30% translucent in the third, built fromSidebarNavin one and from inline anchors in the other two. Two of the three had no header at all abovemd:, so the page had no name on screen and no fixed place for the theme toggle or the account. Every one of those was a defensible local choice, and together they read as three products.AccountMenu, the signed-in identity and the actions that belong to it, at the end of the header. Where it lives is the point: two apps put it in the bottom left corner of the sidebar and one put it in the header. The sidebar is also the worse of the two, because that column is already full height, so opening a menu in its last row pushes the last entry - Sign out, every time - past the bottom edge of the window. It is a nativedetails, so the links and the sign-out post keep working on a page that never hydrated.--spacing-header, the 56px shared by the header bar and the sidebar's brand row. The two meet at the top left corner and a 4px disagreement there reads as a broken seam.
Fixed
VERSIONreported0.13.1from the0.14.0package. The constant was not bumped with the manifest, so a consumer reading the export to decide whether a fix had landed was told it had not.
0.14.0 2026-09-06
Changed
PageShellkeeps the page gutter and the full-size title on afillpage. Owning the viewport is a statement about the content pane, sofillnow drops only the content cap and the content gutter. It used to drop the title row's gutter too and render the heading throughPageHeader compact, which made the pages that own the viewport the only ones in an app whose name sat at body size hard against the window edge: moving between one of those and any other page moved where the page began and changed how large its name was. The gutter composes from the samePAGE_PADtokens, so the two titles now start at the same distance from the edge.
Added
PageShelltakescompact, which drops the title to body size and hides the description. It was previously implied byfilland unreachable on its own, so a page with genuinely no room for a heading had to take the full-bleed content frame to get the small title, and a full-bleed page could not refuse it.
0.13.1 2026-09-06
Fixed
- Controls keep a focus indicator.
CONTROL_BASEandCONTROL_MULTILINEended inoutline-noneand named no replacement, and a utility beats the base layer, so they canceled the global:focus-visibleoutline this theme declares for its contrast. Focus was left as a 1px border-color change on every text input, textarea, number and select in the kit. Both now pair the silenced outline withfocus-visible:ring-2, which is whatCONTROL_SEGMENTandSidebarNav's buttons already did. SidebarNavno longer setsrole="group"on a section's list. That role overrides the implicitlistrole, so every<li>inside sat under a parent that is not a list, which is an ARIA required-context error and a serious axe violation on every admin page that ships a section expanded. Grouping semantics belong to a tree, where the parent is atreeitem; this is a nav of plain links whose disclosure already declares itself througharia-expandedandaria-controls, so the role bought nothing and cost list semantics.
0.13.0 2026-09-04
The kit did not cover the controls the product needed, so every consuming page filled the gap with a raw HTML control. A native select opened in operating system chrome in the middle of a dark theme, a native date input opened the browser's own calendar, and twelve secret fields took an API key with no way to check what was pasted. This release adds the missing controls and, more importantly, the shared contracts that stop the ones already here from drifting apart again.
Added
PasswordInput, a text control with a reveal toggle. The button is a realtype="button", so it cannot submit the form it sits in, and its accessible name states the action rather than the state.TimePicker, with hour, minute and optional second segments in 12- or 24-hour display over a fixed 24-hour wire format. The segments are separate inputs rather than one masked field, because a masked input has a single accessible value and re-announces the whole string on every keystroke.CheckboxGroup, the counterpartRadioGroupnever had, and both now render a real fieldset and legend so the set is announced as a group.SidebarNav, for navigation nested to any depth, with persisted expansion, a collapsed icon rail,aria-currenton the active leaf and its ancestors, and an active rule under which a parent no longer claims its children.TreeView, for nested data, implementing the WAI-ARIA tree pattern with roving tabindex and tri-state checkboxes whose branch state is always derived and never stored.PageShellandSectionHeading. Nothing owned the page gutter, so pages re-declared it in four spellings, five content widths were in play with no rule behind the choice, and fourteen class strings served as a section heading. The gutter, the content cap, the centering and the section rhythm are now properties of the shell.CheckboxandRadiotake an icon, a size, a description, a card variant and a screen-reader-only label, spelled identically on both.Checkboxalso takesindeterminate, which is bindable because the browser clears the DOM property on click and a one-way prop would silently desynchronize.- A richer
Select.mode="listbox"opts into a custom panel with a search field, per-option icons and a custom trigger;mode="native"stays the default and is never inferred from passingoptions, because 28 of the 34 call sites sit inside a form and rely on the browser serializing a real form-associated element. Theonchangeevent signature is frozen for the same reason: many call sites pass an unannotated arrow contextually typed from it, and one readse.currentTarget.form.requestSubmit(). DateTimePicker, composingDatePickerandTimePickerunder one label, one hint and one error, because they are one field.Field, the field furniture as a component.internal/field.tsis private, so a consumer composing a control the kit does not ship had no legal way to match it and hand-copied the class strings instead.FormMessage, for the outcome of a submit. A danger message interrupts withrole="alert"; a confirmation does not.CopyButton,SegmentedControl,Panel,DescriptionList,ToolbarandCollapsible, each replacing something the consuming applications had hand-rolled: a copy affordance with its own timing per page, a radio group drawn as buttons with the selection carried by color alone and no aria state, a lighter grouping that did not want to be aCard, key and value pairs laid out as div grids that convey no relationship, a filter row whose controls sat visibly misaligned, and a disclosure that owns only itself.Cardtakesheading,headingLevel,iconandmeta;Stattakessize,toneandmono. Two apps had each declared their own localStatand the two were visually unrelated for the same job.- Seven internal contracts the components compose from, in the shape of the existing field contract: the option filter, the floating panel, the choice surface, the page layout, calendar and clock arithmetic, and the tri-state roll-up.
FilterFnis public, so a caller can replace the matcher with its own without reaching into a private path. --spacing-panel-max,--spacing-nav-indent,--spacing-nav-railand--spacing-sidebar.
Fixed
MultiSelectandAutocompletehand-rolled the popover, the keyboard model, the filter and the dismissal that the shared modules now own, and each copy carried the same defects.Autocompleteclosed its panel on a 150ms blur timer, so selecting an option worked only because mousedown to click beat the timer. Neither setaria-activedescendant, so a screen reader was never told which row was active. Their option rows were in the tab sequence, so Tab walked into the list instead of leaving the field. Escape did not stop propagating, so one opened inside a modal closed both.MultiSelect's trigger and panel were unlinked in the accessibility tree.- A spread of ARIA attributes hides the role from the compiler, so it could not check
aria-selectedandaria-requiredagainst it. Three components stated the role only through a spread and the armed accessibility gate rejected them once it could finally see them. - The build's accessibility gate had never rejected anything. It tested
startsWith('a11y-')and Svelte 5 renamed every warning code to snake_case, so no violation had matched for the whole life of the Svelte 5 port. RadioGroupkept its focus ring inside the selected branch, so choosing an option removed the only indicator a keyboard user had. That is the defectCheckboxandRadiowere fixed for one release earlier, still live one file away because nothing tested it. It also drew its resting border withline, which reads 1.25:1, and built itsnamefromMath.random().AutocompleteandRadioGroupgenerated instance ids fromMath.random(), which differs between the server render and hydration, so everyaria-controlsandaria-describedbybuilt from one pointed at an element that did not exist on the client.- The Playwright component suite had never run a single assertion. Its
testDirpointed at a directory holding none of the sixteen specs, so every run collected zero tests and exited green. check-distcould only ever reject a specifier beginning with@lyeve, so a component importing a relative path that the published tarball does not contain passed the check. It now resolves every relative specifier indistagainst the file that declares it.- Coverage measured the wrong tree. A user-supplied
excludereplaces the default list rather than extending it, sonode_modulesanddistwere being measured. NumberInputsatisfied the field-contract guard while composing none of it, hand-spelling the border it was supposed to take fromcontrolBorder.FileInputdrew its dropzone boundary withlineat 1.25:1 and its hover with a fractional brand border, on the one element identifying it as a control.Paginationrendered its elision marker as a literal ellipsis character with noaria-hidden, so a screen reader read "horizontal ellipsis" between page buttons.PageHeaderappended its bottom margin ahead of the consumer's class, so a page asking for a different gap shipped two competing margin utilities in one attribute and Tailwind's emitted order picked the winner rather than the page. The margin is now opt-out throughflush, whichPageShellsets because it owns the rhythm itself. It is opt-out rather than removed because 43 pages across the consuming applications render the component directly and take their heading gap from it; deleting it would have moved every one of them by 32px with nothing in their own source to explain why.
Changed
--spacing-card,--spacing-sectionand--spacing-page-ynow carry their measured values. The three described a product that did not exist: card at 24px matched none of the card surfaces in use, section at 48px matched nothing anywhere, and no page rendered an asymmetric gutter. Composing from them is now a cleanup rather than a regression.- The public API guard compares the exported set against the documented list in both directions. It previously checked one direction against a hardcoded count, so a component added to the entry point and not to the list was exported, untested and invisible.
0.12.1 2026-09-03
Fixed
- The published tarball carried a test file.
filesshipssrc/lib/styleswholesale, because thestyles.cssexport points into it, and its test exclusions only coverdist. The contrast suite now sits beside the other library-wide suites insrc/lib, which is not a published path.
0.12.0 2026-09-03
Accessibility pass across the library. The palette and the overlay components both carried defects that every consuming app inherited, and nothing measured either, so the same component was accessible in one app and not in another.
Added
--color-line-strong, for the boundary of a control rather than a divider.--color-linereads 1.25:1 against the surface, which is fine for a rule that carries no information and fails SC 1.4.11 for anything whose border is the only thing identifying it as a control. Inputs, selects, checkboxes, radios, toggles and bordered buttons use the new token; dividers keep--color-line.Toggleaccepts anid, so a<Label for>outside the component can name it.- A contrast suite that parses
theme.cssand measures every token against the grounds it is painted on, including inside a tint of itself, in both palettes. - Consistency guards for two mistakes that had already shipped: a utility class built from a runtime value, and an
aria-modalsurface with no focus handling and no accessible name.
Fixed
ModalandDrawerdeclaredaria-modal="true"while leaving focus in the page behind them, with no focus trap, no initial focus, no focus restore and no scroll lock. A screen reader user was told a dialog had opened and then carried on reading the document underneath it. The behaviorDialogalready had is now one shared action that all three use.Modalhad no accessible name and no height bound, so a dialog was announced as just "dialog" and content taller than the viewport could not be reached.- Dialog stacking generated no CSS. The z-index was written as
z-[{zIndex}], and Tailwind matches complete class names in source text, so no rule was ever emitted and every stacked dialog rendered atz-index: auto. confirm()rejected when the user canceled while documenting that it resolvesfalse, soif (await confirm(...))threw on the ordinary path.- A clickable
Cardtookrole="button"andtabindexfrom itsonclickand then ignored Enter and Space. CheckboxandRadiocarried their focus ring only on the unchecked branch, so ticking a box removed the only indicator a keyboard user had.- Toasts were never announced. Each toast carried
role="status"and arrived with its text already in it, which is not a change to a live region. <Button href disabled>rendered a working link.disabledis not an anchor attribute anddisabled:opacity-50never matches an<a>.Tooltipwas unreachable by keyboard, becausefocusandblurdo not bubble to the wrapper they were bound to; unreachable by screen reader, because nothing pointed at it; and could not be dismissed, which SC 1.4.13 requires.Paginationrendered nothing at all for an empty list, so the "No results" string it carried was unreachable, and it wrote its range with an en dash.- Palette values below the AA floor. On the default dark palette
dangerread 2.85:1, so every form error in the library was unreadable, andviolet3.83:1 inside its own badge. On the light palette the focus ring read 2.39:1, andbrand-lightput near-white text at 2.45:1 on the primary button's hover state.faint,successandwarnfailed onsurface-2. - The exported
VERSIONhad drifted two releases behindpackage.json, sopnpm buildfailed its own version gate and the package could not be built.
0.11.4 2026-09-02
Fixed
- The contributing guide named two private internal applications. This is one of the few repos that is actually public, and the list told a public reader what exists inside the consuming applications for no benefit.
0.11.3 2026-09-02
Fixed
- The README sent maintainers to
PUBLISHING.mdfor the release workflow. No such file exists; the workflow is in CONTRIBUTING.
Changed
- CONTRIBUTING documents the branch model: work branches off
devand the PR goes back intodev, whilemaintakes merges and carries the release tags.
0.11.2 2026-09-01
This release also carries the checkbox fix listed under 0.11.1. That version was written up and committed but never tagged or published, so the registry went from 0.11.0 to here and no consumer ever received it.
Fixed
- The exported
VERSIONstill read 0.11.0 while the package called itself 0.11.1.buildrunsversion:checkfirst, so the package could not be built or republished at all, and anything readingVERSIONgot the release before the one it was running. - A textarea threw away whatever was typed into it before the page finished hydrating. It rendered its value as element content, which compiles to a plain write of the server's value on the first client pass, so a reply typed during the wait was replaced by the empty string the server had sent. The field then failed its own
requiredcheck and the submit did nothing. It binds the value now, which checks the element before writing and keeps what it finds. A rule test holds every textarea in the kit to that.
0.11.1 2026-08-31
Fixed
- A click on a checkbox or radio landed on nothing. The visible box is a decorative element drawn over the real input, and it sat above it in the stacking order without forwarding the event, so the only reliable way to toggle one was the keyboard or a click that happened to land on the label.
0.11.0 2026-08-30
Fixed
- The accordion panel snapped open and its text landed against the header. It now grows to the height of its own content over 200ms, and the body has room to breathe. The height comes from animating
grid-template-rowsbetween0frand1fr, which resolves to the content's own height in CSS alone - nothing measures, nothing reflows per frame, and content of any size works. - A stray colored line under an open accordion header. It was the focus ring: the global
:focus-visibleoutline sits two pixels *outside* the element, and the accordion clips its children, so three of its four edges were cropped and the fourth read as a rule. The accordion, the multi-select options and the toast dismiss button now draw an inset ring, which nothing can clip. Every component that clips and contains a button is checked for this. - The table's row transition had no duration and the table itself had a pointless one.
[&_tbody_tr]:transition-colors duration-150reads as one thought and is not: the bare duration lands on the element carrying the class. Both halves are scoped to the rows now. - The table header read as another body row. It has its own ground.
Added
- The kit honors
prefers-reduced-motion. Every animation ran regardless of what the reader asked for - the drawer slid, the toast flew in, the indicator's ping looped forever. Handled once intheme.css, so it covers components added later too. Durations are reduced rather than zeroed, so anything waiting on ananimationendstill fires. - The accordion wires its header to its panel:
aria-controls,role="region",aria-labelledby, andinerton a closed panel so the tab order and a screen reader agree with what the eye sees.
Changed
- A closed accordion panel stays in the DOM. It has to, for the panel to animate to its own height. It is collapsed to nothing and
inert, so it is neither visible, focusable, nor announced - but a test asserting the body is *absent* when closed will now fail. Assertinertand the collapsed row instead.
0.10.1 2026-08-30
Fixed
homepagepointed atui.lyeve.com, which has no DNS record and has never resolved. The docs site is served fromui-kit.lyeve.com. An audit noted the mismatch in July; the link has been dead on the npm package page since 0.7.0.
0.10.0 2026-08-30
One sweep, so that no component looks like it came from a different library than the one beside it. Two internal modules now hold what forty-eight components each spelled out for themselves, and a source-reading test suite fails when they drift apart again.
Added
labelon Select, Textarea, NumberInput and SearchInput;hinton Select and NumberInput. Every field now offers the same set.classon Button, Checkbox, Radio and Toggle. Button had accepted one only through its rest spread, where it replaced the computed class string instead of adding to it, soclass="w-40"silently dropped every variant and size.aria-invalidandaria-describedbyon every field. Only Input carried either, so a screen reader announced the control and never the reason it was rejected.neutralon Alert, matching Banner, and on Progress, matching the other accent components.--spacing-control, the height of a single-line control, stated once.
Fixed
- the kit shipped its palette tokens and none of the utility classes its own components are built from. Tailwind skips node_modules unless told otherwise, so it never read them. A component rendered only the classes its host app happened to use elsewhere in its own pages, which is why the same component looked different in two apps and why some of it looked unfinished in both.
theme.cssnow declares@source, and every class a component needs comes from the kit. Expect the first build after upgrading to add a few kilobytes of CSS that should have been there all along. - a NumberInput sat two pixels short of the Input beside it: it was the one control whose height was a literal rather than derived from the same padding as the rest.
- Select and Textarea moved their border to
border-brand/50on focus while the other seven fields moved to full strength, and showedborder-danger/70where the rest showedborder-danger. - Select, Textarea and NumberInput spaced their label, control and message row a quarter-step tighter than the six fields around them.
- Textarea rendered its hint underneath its error. Every field now replaces the hint with the error.
- Checkbox was laid out
flexwhere Radio and Toggle wereinline-flex, so it stretched to fill its row while they did not. - Alert, Toaster, Modal, Drawer, Autocomplete, MultiSelect and NumberInput drew their close, check, warning and step icons as the literal characters
x,v,!and-, which take whatever weight the reader's font gives them. All icons are stroked SVG on one grid. - every color transition names its duration rather than inheriting one.
Changed
brandis the canonical name for the cyan tone. Alert, Banner and the toast store called itinfo; Badge, Tag, Indicator and Progress called the same colorbrand.infostill resolves tobrand, sotoast.info(...)andtone="info"keep working - buttoast.infonow records the tone asbrand, which is visible to anything readingtoast.items[].tone.- Button transitions its colors rather than every animatable property.
0.9.4 2026-08-25
Fixed
- avatar falls back to initials for a broken image under a script-src policy with no unsafe-inline, where the inline handler the server renderer emitted was blocked
Changed
0.9.2 and 0.9.3 were tagged but never reached the registry, so this release carries their changes as well. 0.9.1 is the last version consumers can install.
- derive the exported
VERSIONconstant from package.json; the build and the test suite fail when package.json,VERSIONand this file disagree
0.9.3 2026-08-23
Fixed
- brighten dark-palette success and warn tokens to meet AA contrast
0.9.2 2026-08-23
Fixed
- raise the faint token above the WCAG AA contrast floor in both palettes
0.9.1 2026-08-11
Changed
Carries the 0.9.0 changes as well. 0.9.0 was tagged but never published, so this is the first release to reach the registry since 0.8.4.
- Move to node 24 and pnpm 10.33.4.
- Replace em-dashes in comments.
0.9.0 2026-08-06
Added
- page-header: Name the page title for tests.
Changed
- Apply prettier formatting.
0.8.4 2026-08-04
Fixed
- Drop the unused Vite config that made
svelte-packagefail. - Stop shipping the Playwright component test suites in the published package.
0.8.3 2026-07-28
Published with no user-facing changes; repository tooling only.
0.8.2 2026-07-24
Fixed
- Fix broken HTML example and dead external links in README.
- Remove em dashes from README, CONTRIBUTING, CHANGELOG, and package.json.
0.8.1 2026-07-23
Fixed
- Block
javascript:anddata:URIs onhrefprops in Button and Breadcrumb. - Guard against null
name, empty string, and missing accordion context in Avatar and AccordionItem. - Guard against NaN and Infinity in Progress and Pagination.
- Add
typeof documentchecks to dialog manager body-scroll functions so the module loads safely during SSR. - Set loading state before calling
closeDialogin ConfirmDialog to avoid updating a component after it unmounts. - Add
EscapekeystopPropagationin Drawer and Modal so parent listeners don't also fire. - Fix Drawer enter animation direction for left-side variant.
- Add blur handler to Autocomplete to close the option list on focus loss.
Changed
- Replace hardcoded Tailwind colors with semantic design tokens in Dialog, ConfirmDialog, and Toggle.
- Replace unsafe
astype casts with runtime type guards in ConfirmDialog. - Narrow
Record<string, X>to specific key unions in Avatar and Toggle. - Add explicit return types to toast store methods.
- Add
Promise<void>return types to Dialog dismiss and close handlers. - Replace
.tsimport extensions with.jsin dialog components. - Remove redundant
elsebranch in Dropdown$effect. - Add
sideEffects: falseto package.json so bundlers can tree-shake unused barrel exports. - Remove stale
pnpm.overrides.cookie(no longer a transitive dependency). - Remove unused
@types/nodedev dependency. - Loosen
@vitest/coverage-v8from exact pin to^2.1.9. - Remove dead
build:docsCI step. - Remove legacy
packageentry from.prettierignore. - Remove
@sveltejs/kittype annotation fromsvelte.config.js.
0.8.0 2026-07-23
Changed
- Stripped SvelteKit app layer. Ui-kit is now a pure component library. Removed
src/routes/(docs site),@sveltejs/kit,@sveltejs/adapter-static,tailwindcss,@tailwindcss/vite, andvite. The npm package API is unchanged; consumers see the samedist/output as before.
0.7.2 2026-07-21
Fixed
No runtime changes to shipped components.
Dropdownmenu-itemiconis a Svelte component prop; the icon test now passes a real Lucide component instead of a snippet, matching the documented type.- Suppressed a false-positive
a11y_no_noninteractive_tabindexwarning on the clickableCard. The element receivesrole="button"and a focusabletabindextogether wheneveronclickis set. - Dialog tests now consume the rejection from
openDialogwhen a dialog is dismissed, eliminating unhandled promise rejections during the test run.
0.7.1 2026-07-20
Added
- Additional component unit-test coverage (raised to 80%+).
0.7.0 2026-07-16
Added
EmptyStatenow accepts aniconSnippetprop, so consumers can render a custom icon component (e.g. a Lucide icon) in the icon chip instead of only a text/emojiiconstring.iconSnippettakes precedence overiconwhen both are supplied.
0.6.2 2026-07-13
Changed
- Apply prettier formatting to component test files.
- Exclude generated CHANGELOG from prettier check.
0.6.1 2026-07-10
Changed
- Pin pnpm 9.15.0 in mise to match packageManager + lockfile.
- Add render tests for all primitives + logic coverage.
Fixed
- Key Pagination ellipses by index to avoid each_key_duplicate.
0.6.0 2026-07-05
Added
- Imperative dialog stack (openDialog/confirm), release v0.3.0.
Changed
- Refine theme tokens.
- Bump deps and pin cookie>=0.7.0 for CVE fix.
- Sync pnpm-lock.yaml with package.json.
- Apply prettier formatting to satisfy CI format gate.
0.5.0 2026-05-27
Added
- Brand identity.
Changed
- v0.2.0.
0.4.0 2026-05-23
Added
- Interactive playground page.
Changed
- README, CHANGELOG, CONTRIBUTING, MEMORY.
- Add GitHub Actions workflow + PR template.
- Green the install + check + test + package + docs-build pipeline.
- Finish reference pages for the remaining 30 components.
- Pre-publish polish.
- Resolve styles.css to source for file: dev workflows.
- PUBLISHING.md + publishConfig for the public npm push.
- Pin Node 20 via mise to fix crypto.getRandomValues on build.
- Ignore PUBLISHING.md and NOTES.md from git tracking.
0.3.0 2026-05-22
Added
- Wire up the public API surface.
- Scaffold docs site shell.
- Landing page + getting-started + foundations.
Changed
- Add reference pages for the launch set.
0.2.0 2026-05-21
Added
- cn() class merger and theme helpers.
- Toast service backed by Svelte 5 $state.
- The 45-component primitive set.
0.1.0 2026-05-20
Added
- Soft Dark + Soft Light token sheet.
Changed
- Scaffold svelte-package + sveltekit project.