Skip to content

1.18.0

Choose a tag to compare

@github-actions github-actions released this 04 Oct 08:56
· 134 commits to main since this release

Breaking changes

  • A date or a number formatted with no locale is written in en-US on 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: Calendar and the pickers, the charts and their hidden tables, Statistic, AnimateCounter, Meter, the progress indicators, Slider, NumberField, ContextWindow, ColorPicker and DataTable. 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; pass locale, or defaults.locale on a NebaProvider, to keep it in one language from the first paint. The library's own words are unaffected: with no locale they are English, as they were.

  • A run's duration is written in the language of the sentence around it. Reasoning, ToolCall and AgentSteps wrote the duration in the runtime's language and their own words in English, so a Korean browser read "Thought for 2.4초". With no locale the duration is English now, like the words around it; pass locale to have both in the reader's language. It is also never different on the server and in the browser.

  • A closed Sources, Reasoning or ToolCall panel stays in the DOM as hidden="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 as Collapsible and Accordion do 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 check aria-expanded instead; hiddenUntilFound={false} leaves a closed panel out as before.

  • AnimateCounter, AnimateScramble, AnimateSplit and AnimateTyping hold their text once. The animated copy was text in the page beside a hidden copy of the answer, so the server's HTML, textContent and 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 a data-text attribute 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 through textContent should read data-text.

  • StreamingText draws 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 while streaming is on fade now. Turn streaming on 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

  • keepMounted on Tabs, TreeView, Breadcrumb, Drawer, Sidebar, Menu and ContextMenu keeps 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 a maxItems fold 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 of Tabs, TreeView, Breadcrumb and an inline Drawer are in the server's HTML; a Menu, an overlay Drawer and a collapsed Sidebar render into a portal, which mounts only in the browser, so theirs reach only a crawler that runs JavaScript. A TabPanel's own keepMounted still wins over its set's.

  • autoClose on Reasoning. Turned off, the panel stays open when the stream ends instead of folding away and moving the answer below it up the page.

  • headingLevel on Drawer, so an inline drawer's title can sit at the right level of the page outline. It is 2 by default, as before.

  • rel on a BreadcrumbItem and on a ChatBubble's preview, merged with the noopener noreferrer a link that opens a new tab gets, so a card for a URL somebody posted can carry ugc nofollow.

  • priority on Gallery, and srcSet and sizes on a Gallery item. The first priority tiles are fetched eagerly at a high fetch priority and drawn from the first paint, as an Image with priority is. srcSet and sizes reach the tile's <img>, so a small tile is not sent the full-size original; with no full, the viewer chooses from the same srcSet.

  • imageWidth, imageHeight and imageLoading on a HowToSteps step. 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.

  • getPageHref on DataTable, handed to the footer's Pagination, so with paging="pages" the page numbers are links a crawler can follow.

  • DataTable warns in a development build when it draws more than 500 rows with virtual scrolling off by accident: virtual is on but there is no height or maxHeight for it to work with, or there is a groupBy. virtual={false} says it was meant, and does not warn.

  • NebaProvider warns in a development build when its direction disagrees with the dir on <html> at mount. A right-to-left page whose server HTML has no dir="rtl" is drawn left to right until the provider sets it.

Changed

  • A Slider's labels at min and max line 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-busy until 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 ScatterChart redraws 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. A LineChart or an AreaChart no 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 DataTable sorts 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's filter is 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.

  • StreamingText redraws 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.

  • CodeBlock colours 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.

  • Carousel pauses autoPlay while it is scrolled out of view, and holds the slide for a full interval when it comes back.

  • Select lays 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 Image or a Gallery with preview fetches 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 ScrollZone with its buttons inline keeps 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 Combobox works out whether to offer what was typed once per items, and not at all when no custom row can be offered.

Fixed

  • neba installs in a project on Zod 4. The optional zod peer was ^3.25.76, and npm checks an optional peer against the version a project already has, so in a project on Zod 4 every npm install and npm uninstall stopped on ERESOLVE until it was run with --force, whether or not the project used neba/a2ui. The range is ^3.25.76 || ^4.0.0. The adapter builds its schemas with the Zod 3 API at zod/v3, which both majors serve, and @a2ui/web_core installs 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's className wins 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 h2 could not reach them, and that also beat the caller: className="mb-8" rendered with no margin and text-[2.5rem] font-black at 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 and md:text-5xl still has the level's size below md. With no class, a Typography inside .prose or .vp-doc keeps its scale as before, and a ! class such as mb-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's className wins 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, so font-medium or underline-offset-4 lost to it whatever their specificity.

  • Classes on a heading passed as a title win 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 for Alert, Empty and Toast.

  • A Skeleton and an indeterminate ProgressLinear no longer add to the page's layout shift. Their sweeps moved on inset-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 on translate now, look the same, and still run the other way under right-to-left.

  • A server-rendered Image is drawn from the first paint. It sat at opacity: 0 behind 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; an Image first 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 sides fit="contain" leaves. Every Gallery tile follows.

  • An Image or a Gallery with preview no longer suspends in a server render. The lazy Dialog was rendered from the start, so renderToString gave 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, a HowToSteps step and a ChatBubble card write src as the last attribute of their <img>. React 18 sets attributes in the order they are written, and Firefox and Safari start the request when src is set, so loading="lazy", srcset and sizes arrived too late: a lazy picture was fetched at once, and a responsive one fetched src before choosing a candidate. React 19 was not affected.

  • colorSchemeScript, formatFileSize and registerLanguage can be called from a Server Component. They were exported from client modules, so in Next.js the <head> of app/layout.tsx — where the provider page says to call colorSchemeScript() — received a client reference, and calling it threw. Each is in a module of its own now.

  • React 18 no longer prints a useLayoutEffect warning 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.

  • Panes draws each pane at its defaultSize from the first render, the server's HTML included, instead of an even split that jumped to the default sizes after the first paint.

  • PageLayout reserves room for a fixed Header from the first render, at the bar's minimum height for its size, 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.

  • AnimateCounter holds its box at the width of the wider of value and from from the first frame, so the words after it no longer move as it gains digits, and AnimateScramble keeps 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.

  • AnimateSplit and StreamingText hydrate in every browser. Engines cut Japanese and Thai into words differently, and a browser without Intl.Segmenter cut 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 Gallery is 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 the xs column 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 its columns change at, shows only the one for the reader's width with the classes Show uses, and keeps that one, as the same elements, once the page is running. The hidden deals are display: none, so a lazy picture in them is not fetched. A tree rendered only in the browser deals once, as before.

  • A server-rendered SegmentedButton shows 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 a solid set'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 brush hydrates cleanly. The brush is fetched on demand, and a component fetched on demand suspends in a server render: renderToString gave 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.

  • AnimateHeadline puts 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, AnimateScramble and AnimateSplit keep emoji, accents and stacked consonants together, and cut Chinese, Japanese and Thai per character rather than as one piece.

  • Slider reads locale from a NebaProvider. It never asked for it, so a showValue under defaults={{ locale }} was formatted in the runtime's language.

  • A DataTable names its footer's page buttons in its own locale. The footer's Pagination was 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 locale for a stable first paint, the time zone a Date is written in, weekStartsOn for the calendars, calling colorSchemeScript() from a root layout with suppressHydrationWarning on <html>, and putting dir in the server's HTML.

  • The LineChart page says what locale fixes on a server-rendered page and what it does not: the time zone a Date is written in. The provider page says the same about direction and dir.

  • The AppLogo page says to pass the file's width and height through imageProps, so a bare mark keeps its width before the file arrives. imageProps already accepted them.


Every release is in the changelog on the docs site and in CHANGELOG.md at this tag.