Repository navigation
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 running every frame until the button is released. -
StreamingTextredraws only the last 64 words when a token lands, rather than the whole answer: 14 ms per token became 2.2 ms on a 2,000-word answer. -
CodeBlockcolours a block once it is within a screen's height of the view, one block per task, instead of colouring every block on the page in one long task. A block off screen still fetches its grammar straight away. -
CarouselpausesautoPlaywhile it is scrolled out of view, and holds the slide for a fullintervalwhen it comes back. -
Selectlays out only the labels that could be the widest to hold its width, rather than one hidden box per option. A Select of 250 countries carried 250 of them. -
An
Imageor aGallerywithpreviewfetches its viewer when a pointer, the focus or a finger first reaches it, and mounts the viewer on the first press, rather than fetching it with the page. -
A
ScrollZonewith its buttonsinlinekeeps both lanes open, with the buttons hidden, until it has measured itself. The items of a strip that overflows no longer move inward after the first paint. On a server-rendered page, a strip that turns out to fit now loses the lanes at hydration instead. -
Formatting a number or a date costs less per call. A formatter the library already built is found without writing its options out as JSON each time, which was about 7 ms of a dashboard's mount. A
Comboboxworks out whether to offer what was typed once peritems, and not at all when no custom row can be offered.
Fixed
-
nebainstalls in a project on Zod 4. The optionalzodpeer was^3.25.76, and npm checks an optional peer against the version a project already has, so in a project on Zod 4 everynpm installandnpm uninstallstopped onERESOLVEuntil it was run with--force, whether or not the project usedneba/a2ui. The range is^3.25.76 || ^4.0.0. The adapter builds its schemas with the Zod 3 API atzod/v3, which both majors serve, and@a2ui/web_coreinstalls a Zod 3 of its own beside a project's 4; the adapter parses the same messages under either. -
A Tailwind utility in a
Typography'sclassNamewins over the level's own scale. The size, leading, tracking, weight, margins and colour were written at two-class strength so that a host's.prose h2could not reach them, and that also beat the caller:className="mb-8"rendered with no margin andtext-[2.5rem] font-blackat 13px and 400, with no warning. For each property a class sets, with or without a variant, the level now steps down to zero specificity, so the class wins andmd:text-5xlstill has the level's size belowmd. With no class, a Typography inside.proseor.vp-dockeeps its scale as before, and a!class such asmb-8!renders as it did. A class that was being ignored now applies, so check a call site that carried one. A class from your own stylesheet is not read and still needs two classes or!important. -
A Tailwind utility in a
TextLink'sclassNamewins over the link's colour, its weight and its line, for the same reason and in the same way. That covers whether the line is drawn and its thickness, offset and colour. The weight and the shape of the line were a rule in the stylesheet, which a project running Tailwind itself loads outside any layer, sofont-mediumorunderline-offset-4lost to it whatever their specificity. -
Classes on a heading passed as a
titlewin over the sheet's scale.<Card title={<h2 className="text-xl">…</h2>}>rendered at the Card's 15px, because the reset that takes the browser's 1.5em bold and margins off the heading was written at two-class strength. A heading with a class now gets that reset at zero specificity, and one without keeps it at full strength, so it still holds against.prose h2. The same goes forAlert,EmptyandToast. -
A
Skeletonand an indeterminateProgressLinearno longer add to the page's layout shift. Their sweeps moved oninset-inline-start, which the browser reports as a layout shift on every frame, and a consumer's CLS adds those up: a single full-width placeholder scored 0.27 on a page that was otherwise still, past the 0.25 that counts as poor. They move ontranslatenow, look the same, and still run the other way under right-to-left. -
A server-rendered
Imageis drawn from the first paint. It sat atopacity: 0behind its Skeleton until hydration, so the largest picture on a page could not count for Largest Contentful Paint until the JavaScript had run, and with JavaScript off it never showed. A picture that arrived in the server's HTML now has no fade, and its placeholder is drawn under it rather than over it; anImagefirst mounted in the browser fades in as before. Until hydration the placeholder shows through whatever part of the box the picture leaves clear, such as the sidesfit="contain"leaves. EveryGallerytile follows. -
An
Imageor aGallerywithpreviewno longer suspends in a server render. The lazy Dialog was rendered from the start, sorenderToStringgave up on its boundary and React logged a recoverable error at hydration. The viewer is now mounted by the first press that opens it, and its chunk is fetched earlier, when a pointer, the focus or a finger first reaches the picture or the gallery, so that first press does not wait on the network. -
Image,AppLogo, aHowToStepsstep and aChatBubblecard writesrcas the last attribute of their<img>. React 18 sets attributes in the order they are written, and Firefox and Safari start the request whensrcis set, soloading="lazy",srcsetandsizesarrived too late: a lazy picture was fetched at once, and a responsive one fetchedsrcbefore choosing a candidate. React 19 was not affected. -
colorSchemeScript,formatFileSizeandregisterLanguagecan be called from a Server Component. They were exported from client modules, so in Next.js the<head>ofapp/layout.tsx— where the provider page says to callcolorSchemeScript()— received a client reference, and calling it threw. Each is in a module of its own now. -
React 18 no longer prints a
useLayoutEffectwarning for Neba components on a server. Fifteen modules measured themselves in a layout effect, which React 18's server renderer warns about once per component per request; they skip it on a server now, as three already did. -
Panesdraws each pane at itsdefaultSizefrom the first render, the server's HTML included, instead of an even split that jumped to the default sizes after the first paint. -
PageLayoutreserves room for afixedHeaderfrom the first render, at the bar's minimum height for itssize, so a server-rendered page no longer drops by the header's height when it hydrates. A Header wrapped in a component of your own is still reserved only once it has been measured. -
AnimateCounterholds its box at the width of the wider ofvalueandfromfrom the first frame, so the words after it no longer move as it gains digits, andAnimateScramblekeeps its box at the final text's size while it scrambles: each word's noise is drawn over that word and cut off at its end, instead of widening the box or adding a line. -
AnimateSplitandStreamingTexthydrate in every browser. Engines cut Japanese and Thai into words differently, and a browser withoutIntl.Segmentercut everything differently, so the browser drew a different number of pieces from the server and React threw the server's HTML away. The server and the hydrating render now cut the text the same way, and the reader's engine takes over after hydration. -
A server-rendered masonry
Galleryis laid out in the reader's columns from the first paint. A server cannot know how wide the window is, so it dealt the tiles into thexscolumn count and the page redrew itself into three or four columns as it hydrated, moving every tile; the preview demo scored a layout shift of 0.18. Until hydration it now draws one deal for every width itscolumnschange at, shows only the one for the reader's width with the classesShowuses, and keeps that one, as the same elements, once the page is running. The hidden deals aredisplay: none, so a lazy picture in them is not fetched. A tree rendered only in the browser deals once, as before. -
A server-rendered
SegmentedButtonshows its choice from the first paint. The tile under the chosen segment is placed by measuring that segment, which only happens once the page is running, so until then it was a dot in the corner of the groove, and asolidset's chosen label was drawn in its on-fill ink over no fill at all. The chosen segment now draws the tile's own fill itself until the tile is placed, and hands it over in the same frame, so nothing moves or fades when the page hydrates. -
A chart with a
brushhydrates cleanly. The brush is fetched on demand, and a component fetched on demand suspends in a server render:renderToStringgave up on its boundary and React reported the hydration failing. The brush is now drawn once the page has hydrated, over the band the chart already reserves for it, so nothing moves. -
AnimateHeadlineputs a space between its lines in the document, so its text reads "faster quieter yours" rather than one run-together word. -
On a runtime without
Intl.Segmenter,AnimateTyping,AnimateScrambleandAnimateSplitkeep emoji, accents and stacked consonants together, and cut Chinese, Japanese and Thai per character rather than as one piece. -
Sliderreadslocalefrom aNebaProvider. It never asked for it, so ashowValueunderdefaults={{ locale }}was formatted in the runtime's language. -
A
DataTablenames its footer's page buttons in its ownlocale. The footer'sPaginationwas not handed it, so<DataTable locale="ko">with no provider around it still said "Page 2".
Documentation
-
Getting started has a section on server rendering: passing a
localefor a stable first paint, the time zone aDateis written in,weekStartsOnfor the calendars, callingcolorSchemeScript()from a root layout withsuppressHydrationWarningon<html>, and puttingdirin the server's HTML. -
The
LineChartpage says whatlocalefixes on a server-rendered page and what it does not: the time zone aDateis written in. The provider page says the same aboutdirectionanddir. -
The
AppLogopage says to pass the file'swidthandheightthroughimageProps, so a bare mark keeps its width before the file arrives.imagePropsalready accepted them.
Every release is in the changelog on the docs site and in CHANGELOG.md at this tag.