Where Sveltery UI deliberately differs from shadcn/ui's Base UI variant and from Base UI.
Sveltery UI ports shadcn/ui's Base UI variant to Svelte, on a port of Base UI 1.9.0. Upstream is the reference. Components, styles and examples follow it unless an entry on this page says otherwise, and each entry has the maintainer's sign-off. Translations from React to Svelte, such as class for className or snippets for render functions, are not listed.
The upstream links point at shadcn/ui commit d2fba50 and Base UI v1.9.0.
Open state on Base UI triggers
Base UI marks an open trigger with its own attributes. A collapsible trigger gets data-panel-open, and a menu, popover or submenu trigger gets data-popup-open. It never sets data-open or data-state on a trigger, so upstream classes that use them never match.
Sidebar examples and blocks style open triggers with Base UI's attributes
Upstream: Some sidebar examples and blocks style an open trigger with the data-open: variant:
- The collapsible chevron in
sidebar-inset-example.tsxand in thenav-workspaces.tsxof sidebar-10 and sidebar-15 usesdata-open:rotate-90. - The menu buttons in
sidebar-example.tsxandsidebar-icon-example.tsx(also line 385) usedata-open:bg-sidebar-accent. - The menu triggers in the sidebar-01 and sidebar-02 version switchers, the sidebar-07 team switcher, the sidebar-09 user menu, the sidebar-10 actions menu and the dashboard-01 row menu use
data-open:bg-*. - The folder chevron in sidebar-11 rotates on
[data-state=open]on the collapsible root. - All eight styles give
.cn-sidebar-menu-buttondata-open:hover:bg-sidebar-accent, for examplestyle-nova.css.
Sveltery UI: The Sidebar examples and the blocks use data-panel-open: on collapsible triggers and data-popup-open: on menu triggers. Sidebar-11 matches [data-open], which Base UI sets on the collapsible root. The styles use data-popup-open:hover: on .cn-sidebar-menu-button.
Why: With upstream's classes, the chevrons do not rotate and the menu buttons lose their highlight while the menu is open. Upstream's own sidebar-floating-example.tsx already uses data-popup-open:. The .cn-sidebar-menu-button change has no visible effect, because hover: paints the same color.
Decided: Hugo Hsi, 2026-10-14 (blocks) and 2026-10-16 (examples and styles)
Menu sub-triggers stay highlighted while the submenu is open
Upstream: All eight styles highlight an open sub-trigger with data-open:bg-accent. In style-nova.css that is line 451 for the context menu, line 574 for the dropdown menu and line 847 for the menubar. The dropdown menu's dropdown-menu.tsx adds data-popup-open:bg-accent to its sub-trigger, and the menubar reuses that sub-trigger. The context menu's context-menu.tsx does not.
Sveltery UI: All eight styles use data-popup-open:bg-accent on the sub-triggers of the Context Menu, Dropdown Menu and Menubar.
Why: A Base UI submenu trigger gets data-popup-open, not data-open. In upstream's context menu, the parent row loses its highlight when the pointer moves into the submenu. On the ui.shadcn.com context menu preview, an open sub-trigger with the pointer in its submenu has data-popup-open and a transparent background. The highlight is intended: shadcn/ui's Radix variant keeps it, and Base UI's own submenu demo styles [data-popup-open].
Decided: Hugo Hsi, 2026-10-16
Examples copied from the Radix variant use Base UI's attributes
Upstream: The chevron in collapsible-example.tsx rotates on group-data-[state=open]. The bookmark icons in toggle-example.tsx (also line 331) fill on group-data-[state=on]/toggle. These are Radix attributes.
Sveltery UI: The Collapsible example uses group-data-panel-open:, and the Toggle example uses group-data-pressed/toggle:.
Why: Base UI never sets data-state, so upstream's chevron does not rotate and the bookmark does not fill when pressed.
Decided: Under Hugo Hsi's standing rule, 2026-10-16
Upstream bugs fixed on Base
Where upstream code misbehaves on Base UI, Sveltery UI fixes it instead of copying it.
Toggle Group passes its orientation to Base UI
Upstream: toggle-group.tsx takes orientation out of the props and never passes it to Base UI's toggle group.
Sveltery UI: Toggle Group passes orientation to Base UI's toggle group as well as to data-orientation.
Why: Upstream's vertical group keeps horizontal keyboard navigation.
Decided: Hugo Hsi, 2026-10-14
A Toggle Group item outside a group keeps its own variant and size
Upstream: The default ToggleGroupContext in toggle-group.tsx sets variant and size to "default", and the item reads context.variant || variant.
Sveltery UI: Toggle Group leaves variant and size undefined outside a group.
Why: In upstream, an item outside a group ignores its own variant and size.
Decided: Hugo Hsi, 2026-10-14
Tabs pass their orientation to Base UI
Upstream: tabs.tsx takes orientation out of the props for data-orientation and never passes it to Base UI's tabs.
Sveltery UI: Tabs passes orientation to Base UI's tabs as well.
Why: In upstream's vertical example, the list, triggers and panels report a horizontal orientation. The list has no aria-orientation="vertical", and Left and Right still move focus instead of Up and Down.
Decided: Hugo Hsi, 2026-10-14
Slider renders one thumb for a number value
Upstream: slider.tsx renders one thumb per entry of the array value, else of the array defaultValue, else of [min, max].
Sveltery UI: Slider renders one thumb per entry of an array value, and one thumb for a number value or no value.
Why: In upstream, a number value or no value renders two thumbs at the same index. The slider then has two tab stops, and a form posts the name twice.
Decided: Hugo Hsi, 2026-10-14
The Combobox trigger stays out of the tab order
Upstream: combobox.tsx renders InputGroupButton render={<ComboboxTrigger />}. That passes Base UI's Button props to the combobox trigger, and its tabIndex={0} overrides the trigger's own tabIndex={-1}.
Sveltery UI: The Combobox trigger keeps tabindex="-1".
Why: In upstream, the trigger is a second tab stop after the input. Base UI and shadcn/ui's Radix variant (asChild) keep it out of the tab order.
Decided: Under Hugo Hsi's standing rule, 2026-10-16
Blocks work on Base UI
Upstream: These blocks misbehave on Base UI:
- In the sidebar-01 and sidebar-02 version switchers, the menu items use
onSelect, which Base UI menu items never call. Picking a version does nothing. - In the sidebar-09 user menu,
DropdownMenuLabelsits outside aDropdownMenuGroup, which Base UI requires. - The dashboard-01 time-range Select has no
items, so Base UI shows the raw value ("7d") instead of its label. - The dashboard-01 data table resets to page 1 when a reorder replaces its data, so a row moved on page 2 jumps the view to page 1.
Sveltery UI: The blocks use onclick on the version items, wrap the user menu label in the group, pass items to the time-range Select, and set autoResetPageIndex: false on the data table.
Why: Each fix makes the block do what its upstream code intends.
Decided: Hugo Hsi, 2026-10-14
Arrow keys move focus in the Calendar
Upstream: CalendarDayButton in calendar.tsx creates a ref and focuses it when the day is focused, but never passes the ref to its Button. The Radix variant's calendar.tsx passes ref={ref}.
Sveltery UI: In the Calendar, the arrow keys move keyboard focus to the next day.
Why: In upstream, the arrow keys move the focus ring (data-focused) but keyboard focus stays where it was. Enter then selects the day that kept focus, not the one with the ring, and screen readers announce no move. The Radix variant shows the intent.
Decided: Under Hugo Hsi's standing rule, 2026-10-17
Calendar month and year dropdowns have a border
Upstream: The dropdown root in calendar.tsx uses cn-calendar-dropdown-root, which each style defines with a border and focus ring, for example style-nova.css. The registry build leaves that class uninlined, in the served base-nova/calendar.json too, and the site has no rule for it. The month and year dropdowns on ui.shadcn.com have no border and no focus ring.
Sveltery UI: The Calendar dropdowns get the style's border and focus ring.
Why: The style files define the class for this element, so the missing border is a build gap, not a design choice. Without it, a focused dropdown shows no focus ring.
Decided: Under Hugo Hsi's standing rule, 2026-10-17
Example content
The Toggle Group select starts on one of its items
Upstream: The "With Input and Select" example in toggle-group-example.tsx sets defaultValue={items[0]}, an object, while each SelectItem has a string value.
Sveltery UI: The Toggle Group example gives each item the object as its value, so the default matches an item.
Why: In upstream, no item matches the value. Base UI finds no selected item and skips the item-aligned position, so the popup opens from the edge of the trigger instead of over the selected item.
Decided: Under Hugo Hsi's standing rule, 2026-10-16
Every icon in the Button example has data-icon
Upstream: In button-example.tsx, the small outline button's trailing icon has no data-icon. Its sibling buttons all have data-icon="inline-end".
Sveltery UI: The Button example gives that icon data-icon="inline-end" too.
Why: data-icon sets the button's padding next to the icon, so upstream's outline button is spaced differently from its siblings.
Decided: Under Hugo Hsi's standing rule, 2026-10-16
The Dialog form example submits
Upstream: In the "With Form" example of dialog-example.tsx, the form wraps the trigger and the dialog content. The content renders in a portal outside the form, so Save changes does not submit it.
Sveltery UI: The Dialog example puts the form inside the dialog content.
Why: A submit button must submit its form.
Decided: Under Hugo Hsi's standing rule, 2026-10-16
Navigation Menu example links go to Sveltery UI pages
Upstream: The links in navigation-menu-example.tsx point at shadcn/ui's /docs/primitives/* routes, and the Typography link at line 82 points at /docs/primitives/typography.
Sveltery UI: The Navigation Menu example links to the matching /docs/components/* pages. Typography links to #, because these docs have no Typography page. The /docs and /docs/installation links keep upstream's values.
Why: Upstream's /docs/primitives/* paths do not exist on this site, so the links would return 404.
Decided: Under Hugo Hsi's standing rule, 2026-10-16
Sidebar examples fit the preview frame
Upstream: The sidebar examples size the provider with min-h-svh and the sidebar with fixed h-svh, for example in sidebar-example.tsx.
Sveltery UI: The Sidebar examples in the docs use h-full min-h-0 on the provider and absolute h-full on the sidebar.
Why: The docs show each example in a preview frame. A sidebar sized to the viewport would overflow it.
Decided: Under Hugo Hsi's standing rule, 2026-10-16
Forms
The Forms guides cover SvelteKit forms instead of React form libraries
Upstream: The Forms section has a guide for each of React Hook Form, TanStack Form and Formisch, and lists useActionState as coming soon. Each guide keeps the form state in the library and passes it to the Field parts.
Sveltery UI: The Forms section covers a native <form>, SvelteKit remote forms and SvelteKit form actions. Each page uses the same Field parts.
Why: Upstream's guides are written for React. SvelteKit keeps form state in remote forms and form actions, so Sveltery UI's guides show those. Field is a plain div that holds no form state, like upstream's field.tsx, so the same parts work with each of them.
Decided: Under Hugo Hsi's standing rule, 2026-10-16
Toast
Toasts use Toast, not Sonner
Upstream: The Base UI variant ships a Sonner component in sonner.tsx, and its examples and blocks call toast() from sonner, for example in combobox-example.tsx. The site's layout.tsx shows those toasts with the Radix variant's Sonner Toaster at the top center.
Sveltery UI: There is no Sonner component. The examples and blocks import toast from Toast, which is built on Base UI's toast, and call toast.add({ title, description }) or toast.promise(). This includes the Marker and Bubble examples. Toasts show at the bottom of the window, on the right on wider screens.
Why: ui.shadcn.com does not list Sonner among the Base UI components, so the Sonner files in the Base UI variant are left over from the Radix variant. Toast already has Sonner's behavior (stacking, swipe to dismiss, typed toasts, promise toasts), so a second toast system would be redundant.
Decided: Hugo Hsi, 2026-10-14 (no Sonner) and 2026-10-16 (examples)
Rendering and timing
A trigger keeps its own data-slot through render
Upstream: button.tsx has no "use client", and the docs examples are Server Components. Next.js renders <Button> on the server, so a trigger's render={<Button />} arrives as a finished element with data-slot="button", and Base UI's mergeProps lets that slot win. In a Client Component, the same code ends with the trigger's slot, because Button spreads its props after data-slot="button". Upstream shows both: the corner close button in the client file sheet.tsx is data-slot="sheet-close", and the footer close in sheet-example.tsx is data-slot="button".
Sveltery UI: Svelte has no Server Components. An element rendered through a trigger keeps the trigger's data-slot, such as dialog-trigger, popover-trigger or sheet-close, as Base UI does in client code.
Why: It has no visible effect. Only the rhea and luma styles' cn-code-block-actions rule selects data-slot=button, and no code block example puts a trigger there.
Decided: Under Hugo Hsi's standing rule, 2026-10-16
Menus fade in from the first frame
Upstream: On ui.shadcn.com's preview pages, a menu popup mounts with cn-menu-translucent, and the site's menu color script removes that class about a frame later. The first frame paints the menu fully opaque, and then the enter fade starts from transparent.
Sveltery UI: The Dropdown Menu and the other menus start the enter fade on their first frame, as Base UI 1.9.0 does.
Why: The one-frame flash comes from site chrome, not from Base UI or the components. Base UI 1.9.0 and Sveltery UI paint the same frames when they run side by side.
Decided: Under Hugo Hsi's standing rule, 2026-10-16
Data table rows move while you drag
Upstream: The dashboard-01 data table reorders rows in onDragEnd, and React's sortable preset moves the other rows while a row is dragged.
Sveltery UI: @dnd-kit/svelte leaves that live sorting out. The blocks data table reorders in onDragOver and restores the earlier order when a drag is cancelled.
Why: The other rows make room during the drag, as they do upstream.
Decided: Hugo Hsi, 2026-10-14