Repository navigation
Releases: jooy2/neba
Release list
1.19.1
Breaking changes
-
neba/a2uineeds@a2ui/reactand@a2ui/web_core0.12. The optional peers were^0.11.0and are^0.12.0, because 0.12 changed theCatalogthe adapter builds, so upgrade both together with this release. Two changes in 0.12 reach the code around the adapter.MessageProcessorno longer takes its component type from the catalog, so its surfaces type-check asA2uiSurface's only when it is writtennew MessageProcessor<ReactComponentImplementation>([createNebaCatalog()]). And it refuses a surface whose messages name another version than the catalog, or none: every message has to carry the catalog's version,"version": "v0.9"unless theprotocolVersionbelow asks for"v1.0", where 0.11 read any. Everything else the catalog allows keeps working, although 0.12 declares two arguments narrower than it does: anumericcheck still reads a field's text,openUrl'surlmay still be bound to the data model, andformatDatestill writes a date that has no time in Safari, where 0.12 alone writes an empty string. -
The A2UI catalog's
formatNumber,formatCurrency,formatDateandpluralizewriteen-USwhencreateNebaCatalogis given nolocale, rather than the runtime's language, so a server and every browser write the same text.formatDatealso writes the names of months and weekdays in the catalog'slocale, where it used to write them in English whatever the language. Both come from@a2ui/web_core0.12, and the catalog now tells the model so. Pass thelocaleyourNebaProviderhas to keep all four in the components' language. -
A disabled
Accordionsection's header and a disabledCollapsible's header are out of the tab order.@base-ui/react1.9.0 gives the trigger the nativedisabledattribute, where 1.8.0 kept it focusable and marked itaria-disabled, so a keyboard reader no longer stops on a header that cannot open. A test that focused a disabled header should expect the focus to pass it by.
Added
-
createNebaCatalogtakesprotocolVersion,'v0.9'by default or'v1.0', so an agent that writes v1.0 messages can be drawn without rewriting theirversion. Both draw with the same eighteen components and fourteen functions. A v1.0 component'saccessibility.live,accessibility.hiddenandmetadataare accepted and not read, and a check runs as it does under v0.9. One catalog draws one version, so a host whose agents write both keeps aMessageProcessorfor each. -
An A2UI action's
userMessagereaches the host, resolved, beside itsnameandcontext. v1.0 gave an action's event the line a host shows as what the reader asked for, and the v0.9 renderer dropped it; under@a2ui/web_core0.12 it refused the whole button instead. The adapter accepts it, and auserMessagewritten as a path or aformatStringcall arrives as text.
Changed
@base-ui/reactis on 1.9.0, which is the floordependenciesdeclares. It is the only runtime dependency that moved. AChip, aLineChart, anImage, aGalleryand anInlineCitationare each 0.2 kB smaller and aDataTable0.2 kB larger, while a twelve-component app is 0.4 kB larger, a twenty-five-component one 1.5 kB and every export 3.0 kB. Neba's own code weighs what it did.
Fixed
- A
CarouselwithautoPlaybelow the fold no longer turns once before it learns it is out of view. Its timer started straight away and the observer that pauses it answers a frame later, so a shortintervalcould turn the strip once while nobody could see it. The timer now waits for that first answer.
Every release is in the changelog on the docs site and in CHANGELOG.md at this tag.
1.19.0
Breaking changes
-
A
Transfer's lists are narrowed just after a letter reaches the search box, not in the same render. Each keystroke filtered the list and drew the rows it kept before the letter could appear, and every row of both lists, each a full Checkbox, was drawn again on every keystroke and every tick. The box now shows the letter at once and the list follows a moment later, and a tick redraws only the row it changed. A test that checks a row is gone straight after typing should wait for it to leave, withawait expect.element(row).not.toBeInTheDocument()rather thanrow.query(). -
A
TreeSelect's tree is narrowed just after a letter reaches its search box, not in the same render. A keystroke filtered the tree and drew what it kept in the same task, so a short query against a tree of a few thousand nodes, which keeps most of them and opens every branch it kept, held the letter back until all of it was drawn. The box now shows the letter at once and the tree follows a moment later. A test that checks a row is gone straight after typing should wait for it to leave, withawait expect.element(row).not.toBeInTheDocument()rather thanrow.query(). -
A
Sparkline's name is on its box rather than on its<svg>.role="img"and thelabelwere on the strip's<svg>, which is only drawn once the strip has measured itself, so a server render named nothing and a crawler or a reader without JavaScript got a list of numbers with nothing saying what they were. The box that holds the drawing is in the HTML from the start and carries both now. A test that found the strip by its role and read it as the<svg>should take the<svg>inside it. A Sparkline givenwidthas a number also no longer measures its box, and draws once instead of twice when it mounts. -
A legend with
showValuekeeps room for its numbers while no column is active. The number beside each series' name appeared only while the pointer was over the plot, so every row grew as it entered and shrank as it left: a legend under the plot pushed the page below it down and back up, a layout shift of 0.0235 each way on a five-series chart, and one beside the plot took its width from the plot. Each row is now as wide as the widest number its series can show from the start, with the number hidden until a column is active, and the legend no longer changes size. The rows are wider at rest than they were, so a legend under the plot may take another line; a layout that relied on the narrower rows should allow for it. -
A
StreamingTextanswer goes back to plain text once its stream has ended. Every word that streamed in stayed an element of its own for good, so a long chat session kept one per word of every answer: fifteen thousand for fifty answers. Oncestreamingis off and the last word has faded in, the.neba-stream-wordelements are gone and the answer is one run of text, which looks the same. A test that looked for.neba-stream-wordafter a stream ended should look whilestreamingis on, or read the text. A finished word's fade also no longer stays on it as a held animation.
Where the bytes went
| What you import | 1.18.1 | 1.19.0 |
|---|---|---|
Button |
5.0 kB | 5.0 kB |
Chip |
3.4 kB | 5.5 kB |
LineChart |
17.1 kB | 18.1 kB |
CodeBlock |
5.8 kB | 5.9 kB |
Image |
9.0 kB | 9.5 kB |
Gallery |
12.3 kB | 12.8 kB |
DataTable |
65.4 kB | 34.9 kB |
CommandPalette |
38.7 kB | 1.9 kB |
InlineCitation |
31.7 kB | 6.7 kB |
| a whole page shell | 30.5 kB | 13.8 kB |
| 12 components — a typical app | 74.5 kB | 75.6 kB |
| 12 components, with Korean | 78.7 kB | 79.8 kB |
| 25 components — a large one | 122.2 kB | 123.8 kB |
| all exports | 304.3 kB | 317.9 kB |
neba/styles.css |
26.4 kB | 27.2 kB |
Each figure is what a page downloads before it draws, gzipped, with react and react-dom external. The four rows that fell are the parts of Base UI a page used to carry for something closed: a Sidebar's drawer, which a desktop page never opens, a paging DataTable's rows-per-page Select, which a scrolling table never draws, a CommandPalette's whole dialog, and an InlineCitation's preview card. Each is now a chunk of its own, fetched when it can first be needed.
A Chip alone grew 2.1 kB for href and render: the safe rel and href rules and the words a link that opens a new tab ends with, which every link in the library carries, and Base UI's useRender, which a page with a Button on it already has. An Image and a Gallery grew 0.5 kB for the shared observer a Skeleton now holds its sweep still with while it is off screen, and the helper that runs a React 19 ref's cleanup. A LineChart grew 1.0 kB for the hidden table's rows added in runs, the marks laid out once per layout, the transition it draws in after hydration and the room a legend keeps for showValue. The two apps grew 1.1 and 1.6 kB, spread across what they import: useToastActions, a Chip's link support, the sampling of labels that are elements, which a Select and a legend share, a tab bar that binds the wheel only while it overflows, a Select that can be opened from outside and the ref helper.
All exports grew 13.6 kB. About 3.6 kB of that is the code of the new chunks, which a page that imports everything counts as well, and much of the rest is those chunks being compressed as separate files rather than as one. Class strings written as concatenations rather than as an array and a join took 0.1 to 0.3 kB back off every scenario that imports more than one component. The stylesheet grew 0.8 kB for the server-rendered tab indicator, the reduced-transparency surfaces, the text effects' final text without a script, AnimateLighting's composited ring and the rules that hold a waiting entrance in place.
Added
-
autoCompleteonSelectandCombobox, so a browser's autofill can fill them in with the rest of a form:autoComplete="country"on a country list. It is written on the hidden input that submits the value, and a filled-in value that matches an option'svalueor label chooses that option. A Combobox's own text input keepsautocomplete="off", and amultipleCombobox takes no autofill. -
nameonTransfer, so a form submits what is on the right. Each chosen value is a hidden input under that name, in the order ofvalue:FormData.getAll(name)reads them all, and aForm'sonSubmitis handed them as an array. Nothing is submitted while nothing is chosen or while the Transfer isdisabled. -
href,targetandrelonChip, so a chip that leads somewhere, a tag to its page or a filter that is a URL, is a link a crawler can follow and a reader can open in a new tab. The label becomes an<a>that looks and focuses like the pressable label, and theonDeletebutton stays beside it rather than inside it. A link that opens in a new tab getsnoopener noreferrermerged into itsreland says that it opens a new tab, as every other link in the library does. A selected link chip is markedaria-current="true", and adisabledchip is not a link. -
NebaProviderwarns in a development build when the scheme on<html>at mount is not the one it resolves. The provider writesdata-themeonly once it has mounted, so a page withoutcolorSchemeScript(), or with one called with anotherstorageKeyordefaultColorScheme, is drawn in one scheme and changes to the other as it mounts. CallcolorSchemeScript()in<head>with the provider's options. A page with no scheme of its own and a provider onsystemcount as a match. -
initialWidthon every chart, aGaugeChartand aSparklineincluded. A chart is laid out in pixels and a server has nothing to measure, so a server-rendered chart is an empty box of the right height until the page has hydrated. Given a width in pixels, the chart is drawn at it on the server and while the page hydrates, so the drawing is in the first paint and in the HTML a crawler reads, and the measured width replaces it afterwards. It makes the server's HTML larger by the drawing: about 10 kB for a BarChart of a dozen categories, close to 400 kB for a 1,000-point ScatterChart. It is ignored whenheightis a CSS length, and left out nothing changes. -
NavigationMenuItemwarns in a development build when it has both anhrefand children. An item with children opens a panel and is drawn as a button, so thehrefwent nowhere and nothing on the screen said so. Put that destination in the panel as aNavigationMenuLink, or drop the children to make the item a link. A production build says nothing. -
useToastActions():add,close,updateandpromisewithout the list of toasts.useToastalso returnstoasts, which is a subscription, so every component that raised toasts through it rendered again whenever any toast was added, updated or closed. A component that only raises toasts can calluseToastActionsinstead and is never drawn again for that.useToastis unchanged, and theToastActionstype is exported. -
CommandPaletteSlot, the type of the keys aCommandPalette'sclassNamestakes. It was declared beside the component but not exported, so a slots object built apart from the element had no type to name. Import it withimport type { CommandPaletteSlot } from 'neba'. -
renderonChip, so a chip with anhrefcan be your router'sLink. A link chip drew a plain<a>, which reloads the page;render={<Link to="/tags/react" />}now moves without a reload, as it already does onBreadcrumbItem,TextLinkandButton. Keep thehrefon th...
1.18.1
Added
CheckboxGroup: a set of checkboxes that answer one question, any number at once. The set has one value, an array of the ticked checkboxes'values, throughvalue,defaultValueandonValueChange. It is named by itslabel, and itserroris shown under the set and turns every option todanger. Options stand RadioGroup's distance apart, whichspacingandorientationchange.allValueswith aCheckboxthat hasparentadds a select-all box that draws itself indeterminate while only some options are ticked.size,color,readOnlyandnamereach every Checkbox that does not set its own, anddisabledreaches all of them. A native submit sends one entry per ticked box undername, and aForm'sonSubmitgets them as an array.spacingonForm,Fieldset,RadioGroupandList: the gap between what they hold, as an exact length. It is on Tailwind's spacing scale, as onFlexandGridContainer, sospacing={5}is1.25rem. On a Form or a Fieldset it takes over from the gapsizepicks, and a Fieldset's legend keeps its type scale. On a RadioGroup it is the gap between the options, on both axes when a horizontal group wraps. On a List it is the gap between the rows, which otherwise touch, and it is ignored withdividers.NebaProvidertakesspacingindefaults, for the gap between fields across a product. It reaches every Form and Fieldset, and aspacingwritten on one of them still wins. RadioGroup and CheckboxGroup options, List rows,FlexandGridContainerdo not read it, so a product that spreads its fields apart does not spread every set of options and every layout with them.
Changed
- A
RadioGroup's options stand as far apart as aForm's fields, and followsize. A column of options was0.5remapart at every size. It is now0.75rematmd, the gap a Form and a Fieldset put between fields, and0.375rem,0.5rem,0.875remand1rematxs,sm,lgandxl. A horizontal group keeps1.25remalong the row and puts its wrapped lines on the same steps. Passspacing={2}to keep the old column gap.
Fixed
- A toast raised while a modal
DialogorDraweris open is drawn above its scrim, and its buttons take the click. The toast stack read the same--neba-z-portalas every other portalled surface, and on equal z-index the later portal is on top. The stack mounts withToastProvider, so a dialog opened after it covered the toast. The stack now reads--neba-z-toast, which is one above--neba-z-portalunless you set it, so the order holds on a page that raises--neba-z-portal. Menus, tooltips and popovers keep their z-index; where one overlaps a toast, the toast is now the one on top.
Documentation
- Getting started warns against importing
neba/styles.cssin a project that runs Tailwind v4. Its rules sit outside any cascade layer and Tailwind's utilities sit inside one, so a component's own rule beats a class passed inclassNamefor the same property, and only an important class such aspx-10!gets through. Such a project importsneba/tailwind.cssinstead.
Every release is in the changelog on the docs site and in CHANGELOG.md at this tag.
1.18.0
Breaking changes
-
A date or a number formatted with no
localeis written inen-USon the server and while the page hydrates, and in the reader's language right after. It was written in whatever the runtime defaulted to, so a server in one language and a browser in another drew two different strings into the same element, and React threw the server's HTML away and rendered the whole tree again in the browser. This covers every component that writes a date or a number:Calendarand the pickers, the charts and their hidden tables,Statistic,AnimateCounter,Meter, the progress indicators,Slider,NumberField,ContextWindow,ColorPickerandDataTable. A page rendered only in the browser formats exactly as before. On a server-rendered page whose server already spoke the reader's language, the text now changes once, just after it appears; passlocale, ordefaults.localeon aNebaProvider, to keep it in one language from the first paint. The library's own words are unaffected: with nolocalethey are English, as they were. -
A run's duration is written in the language of the sentence around it.
Reasoning,ToolCallandAgentStepswrote the duration in the runtime's language and their own words in English, so a Korean browser read "Thought for 2.4초". With nolocalethe duration is English now, like the words around it; passlocaleto have both in the reader's language. It is also never different on the server and in the browser. -
A closed
Sources,ReasoningorToolCallpanel stays in the DOM ashidden="until-found". It was left out until it opened, so a folded list of sources had no links in a server render and the page's search could not find a word in a closed thought. It works asCollapsibleandAccordiondo now: the content is in the HTML, hidden, and find-in-page opens it. A test that told a closed panel by the absence of its text should checkaria-expandedinstead;hiddenUntilFound={false}leaves a closed panel out as before. -
AnimateCounter,AnimateScramble,AnimateSplitandAnimateTypinghold their text once. The animated copy was text in the page beside a hidden copy of the answer, so the server's HTML,textContentand a copy read "128K 0", "RESOLVING SIGNAL 7@W5#IBM$ KUUWHI", or a line followed by the same line with its spaces gone — and that is what a crawler indexed. The animated copy is drawn from adata-textattribute now, and the answer is the element's only text. The drawn glyphs can no longer be selected; a selection or a copy gets the answer. A test that read the number on screen throughtextContentshould readdata-text. -
StreamingTextdraws text it did not watch arrive as plain text. A message loaded from history, text rendered on a server and text already there when a stream starts were cut into one element per word and faded in, which for a history of fifty answers was fifteen thousand animations starting at once. Only the words that arrive whilestreamingis on fade now. Turnstreamingon while the text arrives to keep the fade.
Where the bytes went
| What you import | 1.17.1 | 1.18.0 |
|---|---|---|
Button |
5.0 kB | 5.0 kB |
Chip |
3.4 kB | 3.4 kB |
LineChart |
16.3 kB | 17.1 kB |
CodeBlock |
5.4 kB | 5.8 kB |
Image |
8.8 kB | 9.0 kB |
Gallery |
11.7 kB | 12.3 kB |
| a whole page shell | 30.1 kB | 30.5 kB |
| 12 components — a typical app | 74.2 kB | 74.3 kB |
| 12 components, with Korean | 78.4 kB | 78.5 kB |
| 25 components — a large one | 121.7 kB | 122.1 kB |
| all exports | 297.7 kB | 302.8 kB |
neba/styles.css |
26.0 kB | 26.2 kB |
The rows that moved are the ones this release is about, and each one paid for a page that is faster to show or to use.
A LineChart grew 0.8 kB, as every chart did: the hidden table's rows are now added after the first paint for a chart past 500 points, the layout is memoised so the pointer stops rebuilding it, and finding a formatter the library already built no longer writes its options out as JSON. That last part is in internal/format.ts, so every component that writes a number or a date carries its quarter of a kilobyte. In exchange, a LineChart of three 1,000-point series mounts in about a fifth of the time and sends 4.3 kB of server HTML rather than 68.6 kB.
A CodeBlock grew 0.4 kB for the shared observer it now waits on before colouring a block, and an Image 0.2 kB and a Gallery 0.6 kB for telling a server-rendered picture from one mounted in the browser, fetching the viewer on the first touch, and the Gallery's priority, srcSet and sizes and the server-rendered masonry. The page shell moved 0.4 kB for the room PageLayout now reserves for a fixed header and the keepMounted a Sidebar passes to its drawer.
The rest of the 5.1 kB on every export is spread across the components below: the text cut the same way on every engine for the animated text, StreamingText's blocks, DataTable's memoised rows and Select's sampler. The stylesheet's 0.2 kB is the sweeps' right-to-left rules and the classes that keep content hidden while keepMounted holds it in the DOM.
Added
-
keepMountedonTabs,TreeView,Breadcrumb,Drawer,Sidebar,MenuandContextMenukeeps hidden content in the DOM. It is off by default, so nothing changes until it is set. On, the panels behind every tab, the rows of a shut branch, the steps amaxItemsfold hides and a closed drawer's contents stay in the document, hidden, so a crawler that does not click can follow the links in them. Those ofTabs,TreeView,Breadcrumband an inlineDrawerare in the server's HTML; aMenu, an overlayDrawerand a collapsedSidebarrender into a portal, which mounts only in the browser, so theirs reach only a crawler that runs JavaScript. ATabPanel's ownkeepMountedstill wins over its set's. -
autoCloseonReasoning. Turned off, the panel stays open when the stream ends instead of folding away and moving the answer below it up the page. -
headingLevelonDrawer, so an inline drawer's title can sit at the right level of the page outline. It is2by default, as before. -
relon aBreadcrumbItemand on aChatBubble'spreview, merged with thenoopener noreferrera link that opens a new tab gets, so a card for a URL somebody posted can carryugc nofollow. -
priorityonGallery, andsrcSetandsizeson a Gallery item. The firstprioritytiles are fetched eagerly at a high fetch priority and drawn from the first paint, as anImagewithpriorityis.srcSetandsizesreach the tile's<img>, so a small tile is not sent the full-size original; with nofull, the viewer chooses from the samesrcSet. -
imageWidth,imageHeightandimageLoadingon aHowToStepsstep. The dimensions are written on the step's<img>, so the browser reserves the picture's proportion and the panel no longer grows as pictures arrive.imageLoading: 'eager'is for a guide at the top of a page. -
getPageHrefonDataTable, handed to the footer'sPagination, so withpaging="pages"the page numbers are links a crawler can follow. -
DataTablewarns in a development build when it draws more than 500 rows with virtual scrolling off by accident:virtualis on but there is noheightormaxHeightfor it to work with, or there is agroupBy.virtual={false}says it was meant, and does not warn. -
NebaProviderwarns in a development build when itsdirectiondisagrees with thediron<html>at mount. A right-to-left page whose server HTML has nodir="rtl"is drawn left to right until the provider sets it.
Changed
-
A
Slider's labels atminandmaxline up inward from their ticks. Centred, the label at either end hung half its width past the track, and in a narrow column past the edge of the column. Every other mark stays centred, and a vertical slider does the same at its top and bottom. -
A chart with more than 500 data points adds its hidden table's rows just after it first draws, in small idle batches, rather than in the server's HTML and the first render. Four 1,000-point line charts and a 5,000-point scatter came to 40,000 table cells and 680 kB of server HTML; a LineChart of three 1,000-point series now mounts in about a fifth of the time, and its server HTML is 4.3 kB rather than 68.6 kB. The caption and the header row are there at once, and the table is
aria-busyuntil it is whole. With JavaScript the table ends up exactly as it was, and a chart of 500 points or fewer is unchanged. -
Charts redraw less while the pointer moves. A
ScatterChartredraws only the marks the pointer left and the ones it reached, and pointing at its legend fades each series of dots as one group instead of starting a transition on every mark: 3,335 animations became 5. ALineChartor anAreaChartno longer retraces its lines as the crosshair moves, and the category axis no longer draws an empty group for each unlabelled category. Where two dots of a dimmed series overlap, the overlap now fades as one sheet, as a dimmed line does. -
A
DataTablesorts once instead of on every keystroke in its search. The sort runs over every row and the search filters what it returns, which is the same rows in the same order; a caller'sfilteris now called in sorted order. Only the rows that changed are drawn again when the active row moves, the body scrolls past a row, a selection the table keeps itself changes or a column is resized, and a render of the table by the caller still draws every row. A dragged selection stops auto-scrolling once it reaches the end instead of...
1.17.1
Fixed
- A
TextLinkthat opens a new tab leaves no space after itself. The space between the label and the "(opens in a new tab)" note for a screen reader was a text node of the link's own, so it was drawn after the glyph and the next word of the sentence stood a space away: "Terms ↗ ." in English, and a Korean particle pushed off its word. The space is inside the hidden note now, and the link's accessible name is unchanged. The same held withicon={false}and with atarget="_blank"written by hand.
Every release is in the changelog on the docs site and in CHANGELOG.md at this tag.
1.17.0
The release the second audit of every public component produced. It went through all of them again, 187 findings in all, and looked hardest at what had landed since the first audit: the agent components, labelPlacement and the notch, the chart brush, the second value axis, references and CSV export, and the A2UI catalog and its adapter.
Most of what it found is accessibility, and most of that is the keyboard and the screen reader. Controls lost the focus when a press disabled them; a DataTable's column widths, column order, counts and multiple sort were out of the keyboard's or the screen reader's reach; links that open a new tab did not say so; live regions read their buttons out or said nothing at all; and small targets were pressed below a finger's size. The pickers are fields of a Form now, a resting float label stops short of what a field draws at its end, a two-axis chart reads each axis in its own units and puts its far ticks on the gridlines, and a server-rendered Calendar hydrates cleanly.
The A2UI half is mostly the catalog telling the truth. The functions declare the arguments the protocol's implementations take, a check may leave out its message, a field that is not bound can still be changed, openUrl waits for a press, a component's accessibility is read, the published catalog names the minor version it belongs to, and the adapter carries a copy of the catalog without the prose, which is about 6.5 kB less on every page that draws a surface.
Eight changes are breaking and come first. A Switch's labelPlacement is deprecated in favour of labelSide, and engines.node is >=18.20.0. Each change below says what it was and what it does now.
Breaking changes
-
ScatterChartandTimelineCharttake nobrushorsecondaryAxis. Both type-checked and neither could be honoured: a brush windows a series by index, which on a cloud of points is an arbitrary cut rather than a range of x, and on a timeline is a cut by row while the spans on it run past the plot; a scatter's second axis wrote its numbers in the first axis' format everywhere but its ticks. They are out of both types, and ignored when passed from JavaScript. Remove them from aScatterChartor aTimelineChartthat sets them. -
A
Ratinga reader sets draws its empty stars at 3:1 against the page. They were the muted ink at 40%, about 1.7:1 on white, which is under what WCAG 1.4.11 asks of a control's state. They are at 80% now, which clears 3:1 in both themes, so an interactive Rating's empty stars are visibly darker. AreadOnlyRating keeps the fainter ghost. -
AnimateCounter,AnimateScrambleandAnimateSplitrender a<span>by default, asAnimateTyping,AnimateHeadlineandAnimateMarqueedo. A<div>is not allowed inside the<span>aStatistic's value is, which is the pairing the AnimateCounter page recommends, and inside a<p>it broke hydration. The root is inline now rather than a block; passrender={<div />}, or ablockclass, where the block was wanted. Their ref is typedHTMLElement. -
An
Alert's live region is its title and message rather than the whole alert.role="alert"orrole="status"was on the root, and a live region is read out whole, so every announcement ended with the action's and the dismiss button's names. The role, and aroleyou pass, go on the column that holds the title and the message now; the ref,classNameand every other attribute stay on the root. A test that found the alert withgetByRole('alert')orgetByRole('status')to read a class or a slot should find the root instead. -
A
HowToStepswith notitlewrites its steps' titles atheadingLevel. They stayed a level below a title that was not drawn, so the default guide under a page's<h2>put its steps at<h4>and skipped a level in the outline. A guide with atitleis unchanged. -
A
ContextWindow's root is a plain<div>, androle="meter"is on the ring, the label and the count. The whole gauge was the meter, and a meter's children are presentational, so the split was not read as a list and a link passed aschildrenwas not a link. The ref,className,styleand every other attribute still go to the root. A test that found the gauge withgetByRole('meter')to read a class or an--n-*slot should find the root instead. -
A
Galleryviewer's Previous and Next keep the focus at the ends. Pressing Next onto the last picture, or Previous onto the first, made the buttondisabled, and the focus went with it. Both stay in place witharia-disabled, as aCarousel's arrows do; a test that assertedtoBeDisabled()on them should assertaria-disabled="true". -
A
Transfer's move buttons keep the focus when the press runs them out. Moving every ticked row made the buttondisabled, and the focus fell to the document, so a keyboard reader moving rows one at a time started again from the top after each. Both stay in place witharia-disabled, as aPaginationstepper does; a disabledTransferstill disables them outright. A test that assertedtoBeDisabled()on either should assertaria-disabled="true".
Added
-
The A2UI adapter reads a component's
accessibility. Every component accepted it and none used it, so an agent that named a group of controls named nothing. On aFlexand aCard,labelmakes the root agroupby that name, and on aButtonwith notextit names the button;descriptiondescribes aFlex, aCard, aButtonand anImage. The components that already name themselves from a prop they draw ignore it. -
A
ScrollAreatakes alabel, and its viewport always has a name. The viewport is a tab stop whenever it overflows, and it had no name, so the focus landed on something that said nothing; anaria-labelpassed to the component named the root instead. Withlabelthe viewport is aregionby that name, and without one it is agroupnamed "Scrollable content" in thelocale's words, as aScrollZone's strip is. -
A
TreeViewmoves to a row by its first letter. Typing a character moves the focus to the next visible row whose label starts with it, wrapping at the end, so pressing it again steps through the rows it starts. Accents and case are ignored, as aDataTablesearch ignores them. -
A
DataTable's columns resize and move from the keyboard. A resize handle was pointer-only and hidden, so a column cut short was text a keyboard reader could not get back. Each handle is now a focusableseparatornamed "Resize" and the column's label (table.resizeColumn, in every registered language), and ← → move it 16px at a time. On areorderabletable, Alt + ← → on a heading moves its column one place, and a heading with no sort button takes the focus for it. Under the pointer, a cut cell withoutrendershows its whole text as a tooltip. -
A
Transfersays what a move did. A polite status reads out how many rows moved and to which list, "2 moved to Selected", in thelocale's words. It istransfer.moved, in every registered language. -
createNebaCatalogtakes alocale. The A2UI functions that write numbers —formatNumber,formatCurrencyandpluralize— followed the runtime's language whatever the components around them were set to, so a Korean surface could say "1,234.5" in a Korean sentence and pick the English plural.createNebaCatalog({ locale })builds them in the language given, which is the one aNebaProvidershould be given too.
Changed
-
A
SwitchtakeslabelSidefor the side its label sits on, andlabelPlacementis deprecated there. On every fieldlabelPlacementsays where the label is drawn — above the field, on its edge, inside it — and aNebaProviderfills it in for all of them; on aSwitchthe same name meant which side,startorend.labelSidetakes those two values now.labelPlacementis still read whenlabelSideis left out, and is removed in the next minor release;SwitchLabelPlacementis an alias of the newSwitchLabelSidefor as long. -
engines.nodeis>=18.20.0. The package said>=18.0.0, andneba/a2uiimports its catalog with an import attribute that Node has only from 18.20 and 20.10. Nothing else in the package needs more. -
The A2UI catalog names the minor version it belongs to. Its
catalogIdwas the barehttps://neba.cdget.com/a2ui/catalog.json, served from the docs, which deploy frommain— so the file behind it could describe components the adapter a host had installed did not have yet. A published catalog'scatalogIdis…/a2ui/<major>.<minor>/catalog.jsonnow, served at that address, and the adapter registers the same id; the bare URL is always the latest. A host that hard-coded the old id in acreateSurfaceshould usenebaCatalogId. -
A pressable
Pilllights and presses its button rather than its whole shell. The pointer light, the hover fill and the press were on the shell, so pressing a control inendIconor inside the opendetailspressed the whole Pill, and the light washed across the details. They are on the pressable middle now; anoutlinePill's edge no longer changes under the pointer, since the edge belongs to the shell. -
useShortcutruns once for a held key, and leaves a key a control already answered. Every auto-repeat of a held combination ran the handler again, and a key a focused field had handled withpreventDefaultran it as well, so a field's ownMod+Kalso opened aCommandPalettebound to the same key. Repeats are skipped unlessrepeat: trueis passed, and still have their default prevented; a key another handler prevented is left to it. ACommandPalettethat is already open no longer asks to open again. -
An
outlineTogglethat is on keeps its plate under the pointer. Hover and press swapped the dyed plate for a paler wa...
1.16.2
Added
labelPlacementon every field: the label above it, on its top edge, or in place of the placeholder.topis the default and what every field drew before.notchstands the label in a gap cut out of the field's top edge, andfloatrests it where the placeholder would be until the field takes the focus or holds a value, then moves it up into that gap. TextField, NumberField, Select, Combobox, DatePicker, TimePicker, DateTimePicker, DateRangePicker, TreeSelect and ColorPicker all take it, typed as the sharedNebaLabelPlacement. A floating label travels ontopand resizes onfont-sizerather than on atransform, so it is set again at each size instead of scaled. AstartIconkeeps afloatlabel in the notch, because the icon is where it would rest. The gap is sized by the layout rather than measured, so it is right in the first frame of a server render, and a field is never narrower than the label on its edge.NebaProvidertakes it indefaultsas well, so a product sets it once.
Every release is in the changelog on the docs site and in CHANGELOG.md at this tag.
1.16.1
Fixed
- A
ContextMenucan sit inside another trigger, which is what aTooltipon the same element needs. 1.16.0 stopped the trigger wrappingchildrenand merged it onto them instead, which is right — and it meant aContextMenuaround aTooltipno longer reached anything: the props were merged onto theTooltip, which describes its own popup with what it is handed and passed none of it on. A tab with a card on hover and a menu on right-click lost the menu. TheContextMenunow passes what it is handed to the area rather than dropping it, so the two compose with the tooltip written around the menu. The other way about still does not work, and cannot while a tooltip's extra props are the popup's.
Every release is in the changelog on the docs site and in CHANGELOG.md at this tag.
1.16.0
Breaking changes
- A
ContextMenumerges onto the element it is given rather than wrapping it. The trigger put a<div>of Base UI's aroundchildren, and nothing could reach that box —classNamegoes to the popup — so a scroll container handed to a menu became a scroll container inside a box with no height, and its content spilled instead of scrolling.childrenis one element now, which the trigger merges onto exactly asTooltip's does, and the menu costs the layout nothing. An area written as several elements, a fragment or bare text has to be wrapped in one element of the caller's own.
Where the bytes went
| What you import | 1.15.0 | 1.16.0 |
|---|---|---|
Button |
5.1 kB | 5.1 kB |
Chip |
3.4 kB | 3.4 kB |
LineChart |
12.6 kB | 15.7 kB |
CodeBlock |
5.5 kB | 5.5 kB |
Image |
8.8 kB | 8.8 kB |
Gallery |
11.7 kB | 11.7 kB |
| a whole page shell | 30.0 kB | 30.0 kB |
| 12 components — a typical app | 71.6 kB | 71.7 kB |
| 12 components, with Korean | 75.6 kB | 75.7 kB |
| 25 components — a large one | 118.7 kB | 118.8 kB |
| all exports | 286.6 kB | 290.4 kB |
One row moved, and it is the one the release is about. A LineChart grew 3.1 kB and so did every other cartesian chart: the reference lines, the export button, the second value axis and the turn arithmetic all live in chart-frame.tsx, which is in all five of their bundles.
What is not in them is the brush. It is 1.5 kB behind a React.lazy of its own, which a chart with no window never fetches, and the CSV writer is another 0.5 kB fetched on the press. Both show in the unbudgeted async column npm run size prints beside every scenario, which is where they can be watched rather than quietly become the entry's problem.
The two app rows moved a tenth each, and neither list holds a chart: that is TextField and Select carrying fieldLight beside trackPointer, and Tabs carrying its alignment map. The page shell has neither and did not move.
Registering a language still costs 4.0 kB. The one word this release added is the export button's, in the chart namespace, and none of those twelve components reads it.
neba/styles.css is 24.6 kB gzipped against 24.5 kB: the rule that puts a field's pointer light out while the reader is typing, and the utilities the brush strip and the export button are drawn with.
Added
-
nullson aLineChartand anAreaChart, with a third answer to a gap.connectNullshad two: break at anull, or bridge it. The one it could not say is the one a caller with an event count wants, which is that a missing row means none happened — sonullstakes'gap','connect'or'zero'.zerorewrites the data rather than the drawing, so the axis takes the nought into its range and the tooltip and the table say0as well; a line pulled to the baseline over a table that still says nothing is a chart disagreeing with itself.connectNullsis now the old spelling of'connect', still read whennullsis left out. -
nullson aSparklinetoo. The same three answers, and they matter more on a strip than on a chart: there is no tooltip and no axis to notice a hole with, so one that quietly closes over three missing weeks is a trend nobody can tell was interpolated.zerorewrites the values, so the range the strip scales itself to takes the nought in and the numbers read out to a screen reader change with it. -
tickAngleonNebaChartAxis, which turns the category labels. Between-90and90degrees. Flat, a long category name has one slot to fit in, so the axis cuts it to an ellipsis and then drops every other one; turned, each label only has to clear its neighbour across its own height, and the room a name needs stops depending on how long the name is. Seven onboarding stages that showed two labels show all seven, whole. The band under the plot grows to hold them, capped at the smaller of 140 pixels and two fifths of the box — past that they are still cut, because a chart that is mostly axis is not a chart. Read on the category axis only, and only where that axis runs along the bottom. -
brushon every cartesian chart — a window on a series a plot cannot hold. Two thousand points is a chart with no points on it: every column is a fraction of a pixel and the shape is a smear. The strip under the plot draws all of them small, the window on it says which part the chart draws, and dragging either handle or the window itself moves it. The handles arerole="slider"buttons rather than shapes inside the picture, so the arrow keys move them and a screen reader is told which category each one sits on — the strip is a sibling of the plot for that reason, sincerole="img"makes everything under it presentational. It is drawn inside the chart's own height, like the axis labels. The window narrows the picture and nothing else: the hidden table and the exported file still hold every point. -
secondaryAxis, andaxis: 'secondary'on a series — two value scales on one plot. For the series whose units are not the others': revenue beside a conversion rate, a count beside a duration. The second axis is drawn on the far edge — the right of a vertical chart, the top of one turned on its side — and its owntickFormatwrites its series' numbers everywhere they appear, the ticks, the tooltip and the table, because the alternative is a percentage printed with the other axis' currency in the one place the two meet. It casts no gridlines: two grids on one plot is graph paper drawn twice, so its ticks land at the same count as the first axis' and the rules already there serve both. Not read on a stacked chart — a stack is a total, and a total across two units is not a number. Worth reaching for rarely, which is why the axis is drawn and labelled rather than merely implied: two scales let a caller make any two series look like they move together, and the reader cannot see that it was done. -
exportableon every chart that has a table — the numbers as a CSV file. A picture is the one form of a number nobody can paste anywhere, and the table a screen reader gets is not reachable with a pointer; this is the pointer's way to the same thing.exportFileNamenames the file andonExporttakes the string instead of downloading it. The module that writes the CSV is fetched on the press rather than imported with the chart, which ishighlight.ts' arrangement in miniature: a page that never turns this on downloads none of it, and no bundle moved. AGaugeChartdoes not take the three props at all — one reading is not a sheet — and the chart's own words gained anexportCsventry in all nineteen languages. -
referenceson every cartesian chart — lines and bands across the plot. A target, an SLA, a budget, the window a forecast covers: the one thing on a chart that is neither a mark nor chrome.valueplaces one andtoturns it into a band;axis: 'category'reads the numbers against the other axis, for a rule that says when rather than how much. The scale widens to hold them, so a target above everything measured is still on the chart, and one that names itself is read out with the data — the picture isaria-hidden, so a reader who gets the table instead would otherwise never learn the target exists. Drawn dashed and neutral unless told otherwise, because a reference indangersays the line is the bad thing when usually the bad thing is the data crossing it. -
xAxisandyAxison aHeatmapChart. It had neither, so its column names could only be thinned by the stride — a grid of long stage names showed two of them and there was no prop to say otherwise. Both are category axes here, since the magnitude is on the colour ramp the scale legend describes, somin,max,tickCountandgridare not read;label,tickFormat,hiddenandthicknessare, andxAxis.tickAngleturns the column names. The arithmetic behind the turn moved intochart.tsasturnedAxis, so a heatmap and a cartesian chart on one dashboard cannot disagree about how deep a band of turned names runs. -
holeandgapon aPieChart.shapealready sets how much of the middle is cut out andholeis the dial behind it, from0to0.92of the radius;gapis the surface showing between two touching slices, in pixels rather than degrees, because the gap is a constant on screen and one that looked right on a 300px chart is a wedge out of a 60px one.holeon apieis also what gives that shape somewhere to put acenter. -
alignonTabs. Where a tab's label sits once the tab is wider than the label is. A vertical bar is the case it exists for: every tab in a column is as wide as the longest one, and centred labels down a column read as a ragged edge on both sides. It moves the label and the icons and nothing else — the tab keeps its size, its padding and its indicator.
Changed
- A field's pointer light is half what a control's is, and goes out while the reader types. A control is the thing the pointer acts on, so the bloom under the cursor is the control answering; a field is on the way to somewhere else, and what the reader looks at a moment later is their own text over the exact patch the bloom is brightest on. So a
TextField,NumberField,Combobox,Select,PromptInputand the date and time pickers takefieldSpotlightSlot, which is the same light at 50%. And because a pointer resting on a field is where the hand left it — over the text, since that is where the reader clicked to get the caret — the one moment the bloom is least wanted is the one moment it cannot get out of the way on its own: a keystroke puts it out and the next point...
1.15.0
Breaking changes
- A
NumberFieldanswers the wheel by default, through a prop now calledwheel.allowWheelScrubwas off, so the one gesture a spinner exists for did nothing until a caller went looking for the prop — and it was a second spelling of a questionScrollZoneandTabsalready ask aswheel. It iswheel, and it is on. Both halves of what it asks still have to be true before anything happens: the field holds the focus and the pointer is over the number, so a reader who has not put the caret in the field scrolls the page over it exactly as before. RenameallowWheelScrubtowheelwhere it was passed, and passwheel={false}for a field inside something that has to keep scrolling under a pointer resting on it.
Where the bytes went
| What you import | 1.14.0 | 1.15.0 |
|---|---|---|
Button |
5.2 kB | 5.1 kB |
Chip |
3.5 kB | 3.4 kB |
LineChart |
12.7 kB | 12.6 kB |
CodeBlock |
5.6 kB | 5.5 kB |
Image |
8.9 kB | 8.8 kB |
Gallery |
11.8 kB | 11.7 kB |
| a whole page shell | 30.0 kB | 30.0 kB |
| 12 components — a typical app | 71.6 kB | 71.6 kB |
| 12 components, with Korean | 75.0 kB | 75.6 kB |
| 25 components — a large one | 118.5 kB | 118.7 kB |
| all exports | 279.8 kB | 286.6 kB |
Six rows went down, and all six by the same tenth of a kilobyte: three shared class strings in internal/styles.ts were […].join(' '), and a bundler cannot drop a const whose value is a call. Every component carried all three whether it read them or not. Written with + they fold at build time and drop, which is why a single-component bundle is smaller in a release that added nine components.
All exports grew 6.8 kB, and no runtime dependency was added. That is the agent group: PromptInput at 5.2 kB of its own code down to StreamingText at 1.2 kB, plus internal/run.tsx and internal/glow.ts, less the fold above. InlineCitation is the one to know about — its preview is a HoverCard, so a citation in a paragraph costs about 35 kB with Base UI's floating machinery, which is why the number is the same whether a page draws one or forty.
Registering a language costs 3.4 kB still; the extra 0.6 kB on that row is the six new message namespaces the agent components read, in Korean.
neba/styles.css is 24.5 kB gzipped against 24.1 kB. It carries the two keyframes a StreamingText word and its caret arrive on, and the classes the nine components use.
neba/a2ui and neba/a2ui/catalog.json are in none of these rows, and that is the point: neither is re-exported from neba, so a bundler walking the package never reaches them. The catalog is 58 kB of the tarball and 0 kB of any bundle.
Added
-
An A2UI renderer to go with the catalog, as
neba/a2ui.createNebaCatalog()returns aCatalogfor@a2ui/react's v0.9 surface with the eighteen components implemented against this library and the protocol's own fourteen functions registered, so an agent's JSON comes out as Neba components with nothing in between to write.The Zod schemas the renderer binds against are derived from
catalog.jsonat load time, rather than written a second time. A hand-written mirror is a mirror that drifts, and the copy that drifts is the one the agent was never told about: a model writing exactly what the JSON allows and a renderer rejecting it. The converter handles the subset the catalog uses and throws on anything else, rather than quietly returningz.any()for a construct it has not met.The functions are
@a2ui/web_core's own.required,formatCurrencyand the rest are the protocol's semantics rather than this library's, and a second implementation of them would be a second chance to disagree with the agent about the one thing both sides have to read the same way.@a2ui/react,@a2ui/web_coreandzodare optional peer dependencies, which is safe here becauseneba/a2uiis not re-exported fromneba— a bundler walking the package never reaches it, so a project that importsButtonand has never heard of A2UI resolves nothing new. Nothing about any bundle moved. -
An A2UI catalog, as
neba/a2ui/catalog.json. A2UI is the protocol for an agent describing an interface as JSON that the host renders with the host's own design system, and a catalog is how the two sides agree on the vocabulary. This one is written against v1.0 and is also served from the URL it names itself by,https://neba.cdget.com/a2ui/catalog.json.Eighteen components and the specification's fourteen functions. Eighteen is the Basic Catalog's count and the number is the decision rather than an accident: a model chooses badly from a list of a hundred and thirty-eight, and what is left out is everything whose useful props are functions,
ReactNodes or render props. The names are the library's own, which makes a host's mapping a lookup rather than a translation, and theinstructionsfield carries the four things a screen gets wrong without being told — that nothing has a margin, that colour is semantic, thatsizeis one ladder, and that a field draws its own label and its own error.The catalog is JSON, so it never enters a bundle and
npm run sizedoes not move; what it costs is about 57 kB of the tarball. Take it alone and the mapping to your components is yours — or takeneba/a2uibelow and it is already written. -
ToolCall— one tool invocation in an agent transcript: the name, what it was called with, what came back and how long it took, with everything but the header folded away behind it.statusis the new sharedNebaRunStatus—pending,running,success,error— and each of the four gets its own shape as well as its own colour, so a reader who cannot tell the two reds apart still has a dashed ring, a turning ring, a tick and a cross.colordecides only what a running call looks like: a failed one isdangerand a finished one issuccesson every ToolCall on the page, because a red row that meant the product's accent colour would have spent the one signal it had.A string
argsorresultgoes into a<pre>with its own line breaks kept, since indented JSON, a stack trace and a diff all mean something by where their lines break; anything else is rendered as the node it is. Leavedurationout and arunningcall counts its own seconds, saying nothing for the first one and stopping the moment a real figure arrives. And a call that fails opens itself, unlessopenis holding it — a reader should not have to go looking for the reason something did not work. -
Approval— the agent asking permission, and the record of what was answered. AConfirmand aPopconfirmare opened by the reader, offer two answers, and are gone the moment one is taken; this is opened by the agent, offers as many as the agent has, and stays, because a transcript in which the question disappears once it is answered cannot be read back to find out what was agreed to.riskislow,mediumorhigh, written out as a word on a chip and never as a colour alone, and it takes over the card's family.No option is emphasised by default. Every button is
outlinein the card's family and an option's ownvariantis how one is singled out — a permission request whose loudest button is "Allow" is a request answered by the shape of the buttons rather than by the reader. -
Reasoning— a thinking panel that the stream opens and closes. On aSpoilerand aCollapsiblethe reader owns the open state; here the stream does, which is the whole of what the component is for: the panel fills while the model is working, folds itself away when the working stops, and leaves one line saying that it happened and how long it took. It follows the edges ofstreamingrather than its value, so a reader who folds it away mid-stream is not overruled on the next token, and one that mounts mid-stream starts open because no edge is coming to open it.variantdefaults totexthere and nowhere else in the library: thinking is an aside, and a bordered box around every aside in a conversation is a conversation made of boxes. The panel is deliberately not a live region — thinking is long and is revised as it arrives, and a screen reader reading every revision aloud would bury the answer it is on the way to. -
AgentStepsandAgentStep— a chain of steps that grows as it runs. ATimelineand aHowToStepsdraw a list that is known in advance; this one does not know how many items it has, so a step's place is not a prop and appending or inserting one renumbers nothing.runningdraws one more marker under the last step for the case where the next step has no name yet, with no visible label, because the turning ring has already said what it is and the status is read out either way.A step takes the same
NebaRunStatusaToolCalldoes, and the same four marks;successis its default, since a step that is already in the list has usually already run. Itschildrenhold whatever it did — a query, a file, a wholeToolCall. -
ContextWindow— how much of the context window has gone, and what went into it. AMeterdraws the same reading as a bar and would do most of it; what it cannot do is the part that makes this a component: token counts are five and six digits long and have to be written compactly in the reader's own language, the split into input, output, reasoning and cached only means anything with the four next to each other, and the money underneath is a third unit again.useddefaults toinputplusoutputplusreasoning;cachedis drawn as a row and never added, because every model that reports it means "this much of the ...