Repository navigation
1.9.0
Sixteen components, and they come from one question asked properly: what does a large application still have to write by hand after installing this? The answer was in two places. Nine of them were Base UI primitives that had simply never been wrapped — a toggle and its group, a meter, a menu bar, a navigation menu, a hover card, a scroll area, a form and its fieldset. The other seven are the components a product team writes itself around the third month: a command palette, a guided tour, a two-list transfer, a table of contents that follows the scroll, a key-value panel, a stack of avatars, and a gauge.
Twenty new exports, and the standing scenarios say what they cost the people who do not import them:
| What you import | 1.8.1 | 1.9.0 |
|---|---|---|
Button |
5.0 kB | 5.0 kB |
Chip |
3.0 kB | 3.0 kB |
LineChart |
11.0 kB | 11.0 kB |
| a whole page shell | 28.1 kB | 28.1 kB |
| 12 components — a typical app | 67.0 kB | 67.1 kB |
| 25 components — a large one | 110.8 kB | 111.4 kB |
DataList |
— | 1.4 kB |
Anchor |
— | 2.0 kB |
Meter |
— | 3.9 kB |
Form with Fieldset |
— | 3.9 kB |
GaugeChart |
— | 5.4 kB |
CommandPalette |
— | 36.6 kB |
| all exports (126 → 146) | 215.3 kB | 239.8 kB |
The four unchanged rows are the ones worth reading: twenty new exports, and a bundle that did not ask for them is the same size to the byte. The two application-sized rows moved by 0.1 kB and 0.6 kB, and for a reason that is in Changed below rather than for anything that stopped being shaken out — every field component now renders the message its validity already had.
The spread in the new rows is Base UI rather than Neba. DataList is a <dl> and weighs what one weighs; CommandPalette is a modal dialog over an autocomplete, and the floating machinery underneath it is most of thirty-six kilobytes — shared with Menu, Select, Popover and Dialog the moment a page uses any of them. neba/styles.css moves 19.7 kB → 20.8 kB gzipped.
Added
-
ToggleandToggleGroup. A button that stays down. The difference from aSwitchis what the press is — a switch changes a setting and the change is the point, a toggle changes the state of the thing beside it: bold on the selected words, the grid on the canvas, the filter on the list. The difference from aCheckboxis that this one is a control rather than an answer, so it never goes in a form.variantsays how the toggle looks while it is off, and on is always the colour family asserting itself. That is the whole design: the ink at rest is--neba-muted-fgin all three weights, because a Button at rest is an action waiting to be taken while a toggle at rest is a state that is currently false, and accent ink on an unpressed toggle says it is on. With no children it goes square around its icon, which is the shape a toolbar wants.ToggleGroupsquares off the corners facing a neighbour exactly asButtonGroupdoes, owns the value as an array in both the single and the multiple case, and setsvariant,size,color,density,elevationanddisabledonce for the set. It provides the same context aButtonGroupprovides rather than a second one spelled identically, so aToggledropped into aButtonGrouppicks the set up too. -
Meter. How much of something there is, on a scale known in advance — disk used, seats taken, quota spent, a password's strength. It looks exactly like aProgressLinearand is not one: a progress bar is about time, so it may have no value at all and is expected to move on its own, while a meter is about quantity and does not move unless the thing it measures does.valueis therefore required, androle="meter"rather thanrole="progressbar".thresholdsis the reason it earns a component. A meter's whole job is that where the value sits is what it means — 40% of a disk is fine, 95% is a page — and left to the caller that is a ternary at every call site, with the fourth one disagreeing about where amber starts. -
GaugeChart. The same reading bent into an arc, and deliberately the same component in two shapes:value,min,maxandthresholdsmean exactly what they mean on aMeter, so a figure can move from a bar to a dial without changing what it says. Reach for the bar in a row of fields and for the dial in a tile of its own, where it reads from across a room and four pixels of bar does not.sweepopens the dial symmetrically about twelve o'clock —180for a dashboard tile,270for the instrument shape,360for a ring — and the drawing is sized against the box for the sweep it was given, so a half-dial leaves no empty half above it. It is not aPieChartwithshape="semi": a pie is parts of a whole and every slice is a category, while the unfilled part of a gauge is not a second category, it is the rest of the dial. The reading in the middle is real text rather than an SVG<text>, so it is selectable, findable and in the accessibility tree. -
Menubar. The strip of words at the top of an application — File, Edit, View. What makes it a bar rather than a row of separate menus is what happens once one is open: moving along the strip walks through the others instead of closing the one you left, and the arrow keys move between the menus as well as inside them.MenubarMenutakes alabeland the sameMenuItem,MenuGroup,MenuSeparator,MenuCheckboxItem,MenuRadioGroupandMenuSubmenuaMenutakes, because it is the same menu.It draws no surface, and the words sit on a ladder one rung below the control heights at every step. A menu bar is a strip of words rather than a row of buttons, and it is always on something that already has a height — a
Toolbar, aWindowPane's title bar, aHeader. Sized as controls,File Edit Viewwould make the bar taller than the thing it is drawn on. -
NavigationMenu. A site's navigation: a row of destinations, some of which open a panel of more of them. The difference from aMenuis what the rows are. A menu holds actions, so its rows aremenuitems and the whole thing is a widget. This holds links, so it is a<nav>full of real<a href>s — which is what puts them in the link list a screen reader pulls up, on the status bar, in a "open in new tab", and in a crawler's index. An item with children is a trigger and a panel; an item with anhrefand nothing else is a link, and the two are announced differently.One panel is open at a time and it resizes between items rather than closing and reopening, so crossing the row reads as one surface rather than three.
columnslays a wide panel out in two or three. -
CommandPalette. Everything an application can do, behind one field — the shape a keyboard-first product takes once it has more actions than a menu bar can hold. Not aMenu, which is a short list in one place where every row is visible before you look for it; not aComboboxeither, because what comes back is not a value, it is something happening.keywordsis the prop that decides whether anyone opens it twice: words the query matches and that are never drawn, soRoll backis found by typingundoandDeploy productionby typingship.groupdraws a heading each time it changes.shortcutbinds the opening keystroke on the window and defaults toMod+K—Modbeing Command on a Mac and Control everywhere else, read from the same spellingShortcutdraws, so the label on the screen and the key that actually works cannot drift apart.shortcut={false}binds nothing, for an application that owns its own keyboard. -
Tour. A guided walk over a page that already exists — the three things a new reader has to be shown once, pointed at where they actually are. It isHowToStepsturned inside out: that component puts the instructions in the page and the reader follows them, this one leaves the page alone and stands over it. Each step names its target with a CSS selector rather than restating it, because what a tour is about is already on screen and a second copy inside the card is a second copy to keep in step.The dimming is one element with a hole in it — a box the size of the target carrying a shadow larger than any screen — rather than four rectangles around it, because the corners of a four-piece scrim never quite meet and the seams show the moment the dimming is anything but opaque. It never takes the pointer either, so the control being pointed at can still be used while the card is up, which is the whole difference between a tour and a sequence of dialogs. A step with no
targetis centred with nothing cut out, for a welcome card and a closing one. -
Transfer. Two lists and the arrows between them: everything that could be chosen on one side, everything that has been on the other. The shape for a choice that is long — the columns in a report, the permissions on a role, the people on a channel — where aComboboxwith forty chips in its field stops being readable and forty checkboxes give no answer to "what did I actually pick". Below about a dozen options, one of those two is the smaller component.Ticking a row is not choosing it: ticks say which rows the next press moves and
valuesays which side they are on, and keeping the two apart is what lets a filter hide a row without silently moving it. The order ofitemsis the order both lists show, so a row does not jump when it is sent across and back. -
Anchor. The list of headings on the page being read, with the one the reader is in marked. Real<a href="#…">s in a real<nav>: they jump to their headings with JavaScript off and they are in the link list, and the scroll tracking is added on top rather than being the thing that makes it work.The marked row is the last heading whose top has passed the line, which is the only rule that reads correctly going up as well as down, and the last heading wins once the scroll reaches the bottom — otherwise a final section with less content than a viewport is the one section that can never be marked. Nothing is marked while the reader is still above the first heading.
offsetclears a sticky header andcontainernames what scrolls when it is not the document, which is what aPageLayoutwithscroll="content"needs. -
DataList. A list of things and what they are called — a details panel, a summary of a record, the metadata under a heading. Real<dt>/<dd>pairs rather than a two-columnTable, and the difference is not cosmetic: a table is a grid of rows all of the same shape and a screen reader walks it as a grid, while this is a set of pairs and each one is read as "label, value". The label column sizes itself to the widest label, so every value in the list starts at the same place without anybody measuring. -
AvatarGroup. A stack of avatars, overlapping, with the ones that did not fit as a count.maxis how many are drawn andtotalhow many there are altogether, for a group handed only the first few. The first avatar is on top, because a stack read left to right is read front to back and the one the group is about should not be the one behind everything else. -
HoverCard. A card that opens when the pointer rests on something, holding a preview of what is on the other side — a person behind a mention, a repository behind a link, a deploy behind an id. It sits between the library's other two popups and is close to both: aTooltipis a label the pointer never reaches, aPopoverwas asked for by a press. This one is uninvited like the first and reachable like the second, so the pointer can cross into it and a link inside it can be followed.closeDelayis what makes the gap between the trigger and the card crossable. -
ScrollArea. A box with a scrollbar of its own, because the browser's is drawn by the operating system: seventeen pixels wide on one machine, overlaid and invisible on the next, and a different colour from the sheet it is cut into. NotScrollZone, which is a rail — a strip of items with buttons that step through them; this is the plain case of a box too small for what is in it, and underneath both are ordinary scroll containers, so the wheel, the trackpad, momentum and the keyboard are the browser's own.fadedims the content at each edge that has more beyond it and only there, so there is no fade at the top when you are at the top. It is a mask rather than a gradient painted over the content, because a gradient has to fade to a colour and a scroll area usually sits on a translucent acrylic sheet where there is no such colour. -
FormandFieldset. A<form>that knows which of its fields is wrong. On its own, a page ofTextFields validates one field at a time and a failed submit leaves the reader to find the red one; what this adds is the part that has to be owned above the fields — a submit collects every field's validity at once and moves focus to the first that failed, so the reader is taken to the problem rather than told there is one.errors, keyed by each field'sname, puts a server's answer back on the field it belongs to instead of in a banner, and clears it as soon as that field changes.It is not a form library. There is no schema, no resolver and no field array here — a project that wants those keeps them and hands the result to
errors, which is the seam this is built around.validationModedefaults toonSubmit, the only setting that does not tell somebody their email is wrong while they are still typing it.Fieldsetis the grouping and draws no surface, because a group of fields is a grouping and not a sheet — the sheet already exists, and this goes inside aCard. What it owns is the legend, the gap the controls stand at, and the one thing only a real<fieldset>can do:disabledreaches every control inside it, including ones a component three levels down rendered and never heard of it. -
Three new message namespaces in all eighteen languages —
anchorfor the table of contents'<nav>name,transferfor the two headings, the two buttons, the filter and the select-all, andcommandfor the palette's placeholder, its empty line and the name of a dialog that has no visible title — plussteps.skip, whichTourandHowToStepsshare. All four together move a twelve-component app with Korean registered from 69.2 kB to 69.6 kB. -
ScrollZonetakes the wheel, where it is asked to. A mouse has one wheel and it points down the page, which is the one axis a horizontal strip does not run along, sowheelturns a wheel rolled over the strip into travel along it. It is off by default, because a wheel taken from the page is the page's: a reader who meant to scroll past the shelf would be held by it instead. What it does take it gives back — at either end of the strip the wheel is the page's again — and a trackpad swiping sideways is left alone, since that already scrolls the strip and answering it here would move it twice as far as it was asked to.
Changed
-
A field with no
errorof its own now shows the message its validity already had.TextField,NumberField,OtpField,Select,Combobox,Checkbox,RadioGroupandSwitchrendered a message only when the caller passed one, so arequiredfield that failed the browser's own constraint went red and said nothing, and aForm'serrorsmarked a field invalid without ever drawing the sentence. Each of the eight now falls through to a plainField.Error, which renders the current validation message and nothing at all while the field is valid. An expliciterrorstill wins and still shows whenever the field is invalid. This is the tenth of a kilobyte in the twelve- and twenty-five-component rows above. -
Menuno longer passesmodalwhen it was not given one. Base UI's default is the sametrue, and it warns when the prop is set on a menu that turns out to be nested — which is every menu on aMenubarand every submenu. Not passing it is how the default stays a default; nothing about how a menu behaves has changed. -
NebaThresholdis in the shared vocabulary, insrc/types.tsbesideNebaSizeandNebaColor, becauseMeterandGaugeChartare the same reading in two shapes and a page carrying both must not disagree about where amber starts. It is the one place in the library where a semantic colour is computed. -
fill()— the{index} of {total}interpolation — moved fromHowToStepsintointernal/i18n.ts, now that a second component counts steps. The placeholders are part of the message format, and a language that orders them the other way round is exactly the case a second copy of that function would eventually get wrong. -
ScrollZone'sbuttonPlacementnow defaults toinline. The buttons stand beside the strip rather than over its ends, so an item is cut off at the button's edge instead of sliding beneath it, and the button is legible over the page rather than over whatever it landed on.overlayis still there and is still what a shelf of pictures wants, where the thing under a button is a picture that carries on. The type listsinlinefirst now, the way every other union in the file puts its default first.
Documentation
- Thirty-two component pages — sixteen components in both locales — with their props rows, sixty-one demos, sixteen cards in the component gallery and their blocks on the sample screen.
llms.txtand the README's component lists carry all sixteen. - The sample screen gains the two components that are only really visible in a page that already exists: a
Tourhung off the application header it points at, and aCommandPaletteopened by theMod+Krow the Keyboard card was already advertising. - One hundred and eighty new test cases across sixteen files, taking the suite to 2,228.
CLAUDE.mdrecords the new group memberships, the two internal modules that gained a second reader (button-group.tsforToggleGroup,menu.tsforMenubar), the newinternal/avatar-group.ts, and the refreshed bundle table.
Every release is in the changelog on the docs site and in CHANGELOG.md at this tag.