Releases
Upgrade Guide
v0.50.0

Upgrading to v0.50.0

This guide lists every breaking change in the release with the matching migration steps and code samples.

Upgrade Guide: v0.40.0 → v0.50.0

v0.50.0 is an engine-level rebuild: the date, table and form engines are all replaced, the Sheet/Drawer families swap names by semantics, and headless trims its presentation-only shells. Minors may break during 0.x, and this release ships no compatibility layer — every change gets a migration mapping below.

Three things you want to know first
1. The date value model is now the native Date; CalendarDate / ZonedDateTime are gone
2. Table columns and state use TanStack naming as the first citizen — legacy fields get no aliases
3. Apps that only consume @vean/ui (S components) mostly just need the renames

1. Date engine: @internationalized/date → date-fns + Date

@internationalized/date and @internationalized/number are replaced by date-fns plus the platform Intl APIs. The public value model becomes the native Date; component props/slots stay compatible and only the value types change. The first release supports local dates only (ZonedDateTime, non-Gregorian calendars and timeZoneName segments are removed; hideTimeZone stays as a no-op for one release cycle).

v0.40.xv0.50.0
CalendarDate (calendar day)Date at local midnight
CalendarDateTime (date + time){ date: Date, time?: Date }
Time (time of day)Date (only h/m/s matter, normalized to a fixed epoch)
DateRange / TimeRange { start, end }{ start: Date | null, end: Date | null }
DateMatcher / TimeMatcher(date: Date) => boolean predicates
- import { CalendarDate, CalendarDateTime, Time } from '@internationalized/date';
- const day = new CalendarDate(2026, 4, 19);
- const time = new Time(9, 30);
+ import { createDate, createTime } from '@vean/aria/date';
+ const day = createDate(2026, 4, 19); // local midnight, month is 1-based
+ const time = createTime(9, 30);

Exported type names (DateValue, DateRange, TimeValue…) are unchanged — only their shapes became Date. Calendar math moves to date-fns functions (addDays / addMonths…) or the kernel helpers from @vean/aria/date (setSegmentParts / cycleSegment / toISODateString), and number formatting moves to createNumberFormatter / createNumberParser from shared (thin Intl.NumberFormat wrappers).

pnpm remove @internationalized/date @internationalized/number
pnpm add date-fns

The full operation / formatting / parsing mapping lives in the dedicated guide: Date engine migration.

2. Table engine: @tanstack/vue-table

The hand-rolled column model (sorting / filtering / expanding / pinning / sizing) is replaced by @tanstack/vue-table v9. TanStack naming is the first citizen and legacy fields get no aliases (a deliberate design decision) — rename them by hand:

v0.4x (legacy)v0.50.0 (TanStack)
{ title, dataIndex }{ header, accessorKey }
sorter: true / sorter(a, b)enableSorting: true / sortFn: (rowA, rowB) => number
filter: true / filter: { ... }enableColumnFilter: true / filterPlaceholder / filterOptions
width: '140px' / minWidthsize: 140 / minSize (px numbers)
group column { key, children }{ id, columns }
sortState / defaultSortStatesorting / defaultSorting (SortingState)
filterState / defaultFilterStatecolumnFilters / defaultColumnFilters (ColumnFiltersState)
columnWidths / defaultColumnWidthscolumnSizing / defaultColumnSizing (Record<string, number>)
expanded: R[]expanded: ExpandedState ({ '1': true } or true)
update:sortState / update:filterStateupdate:sorting / update:columnFilters
  const columns = [
-   { title: 'Name', dataIndex: 'name', sorter: true, width: '180px' },
+   { header: 'Name', accessorKey: 'name', enableSorting: true, size: 180 }
  ];
- const expanded = ref([1]);
+ const expanded = ref({ 1: true });

The engine instance is not hidden: slot props carry table / engineColumn, subtrees can call useTableEngine(), the template ref exposes the engine directly (tableRef.value.previousPage()), and advanced TanStack options pass through tableOptions.

The full field mapping with examples lives in the Table docs “Migration from v0.4x”.

3. Form engine: @tanstack/vue-form

The self-built useHeadlessForm is replaced by @tanstack/vue-form. The component face is unchanged — SForm / SFormField / SFormFieldArray / SFormFieldBase plus the label/description/error and aria-invalid / aria-describedby wiring all stay. The changes concentrate on the hook face:

v0.4xv0.50.0
useForm(options, Field, FieldArray) tupleuseForm(options) returns a context object (handleSubmit, SFormField, isSubmitting, form…)
initialValuesdefaultValues (TanStack naming)
onSubmit(values, helper)onSubmit(values) (FormSubmitHelper removed)
formState.values / errors state snapshotform.useSelector(state => …) / form.Subscribe (the raw form.state remains on FormApi)
reValidateMode / field-level reset callbackRemoved — set validateMode: 'change' for live validation; reset restores defaultValues
Nested array path social.0.nameTanStack canonical social[0].name (error paths use brackets too)

Validation accepts a Standard Schema object directly (Zod / Valibot both work); @standard-schema/spec is no longer a direct dependency of the library — your own validation library provides it. Field-level validate accepts a function or a Standard Schema validator, and passing it through a Ref rebinds rules reactively.

The complete hook-face mapping and caveats (array error paths, the _form key, formOptions passthrough) live in the Form docs “Migration from v0.4x”.

4. NavigationMenu removed

SNavigationMenu and the whole navigation-menu family (headless sub-path, root exports, the Headless.* namespace, the registry entry) are removed; nav-menu is the only navigation menu family:

- import { SNavigationMenu } from '@vean/ui';
+ import { SNavMenu } from '@vean/ui';

- import { NavigationMenuRoot } from '@vean/aria/navigation-menu';
+ import { NavMenuRoot } from '@vean/aria/nav-menu';

Root props are nearly isomorphic; sub-menu flyouts move from “indicator + viewport” to nested popovers beside the item (NavMenuSubTrigger / NavMenuSubContent), and mount control switches from unmountOnHide to forceMount on the content primitives. The full mapping lives in the migration notes (kept for one release cycle) and the NavMenu docs.

5. Sheet / Drawer renames

Two families swap names so the semantics are explicit: side panels are Sheet, gesture drawers are Drawer.

SDrawer (side panel) → SSheet — still composed from the Dialog primitives, behavior is identical, a pure rename:

LegacyNew
SDrawerSSheet
drawerVariantssheetVariants
DrawerProps / DrawerEmits / DrawerSlotsSheetProps / SheetEmits / SheetSlots

SBottomSheet (gesture drawer) → SDrawer — the family renames wholesale, and the kernel is rebuilt on the Base UI model:

LegacyNew
SBottomSheet / BottomSheetRoot / …SDrawer / DrawerRoot / … (prefix maps one-to-one)
@vean/aria/bottom-sheet@vean/aria/drawer
v-model:active-snap-pointv-model:snap-point
direction prop (headless)side prop
data-vean-bottom-sheet-*data-vean-drawer-*
shouldScaleBackground / fadeFromIndexRemoved — wrap the page in DrawerIndent + DrawerIndentBackground; overlay opacity now fades continuously with the swipe
- <SBottomSheet v-model:open="open" v-model:active-snap-point="snap" :snap-points="[0.5, 1]">
+ <SDrawer v-model:open="open" v-model:snap-point="snap" :snap-points="[0.5, 1]">
    <template #trigger><SButton>Open</SButton></template>
    Content
- </SBottomSheet>
+ </SDrawer>

Note that the snapPoint default changed from null to the first entry of snapPoints (a drawer opens snapped to its first snap point). Rename and engine details live in the Sheet docs and the Drawer docs.

6. headless anatomy shells trimmed (skip if you only use the UI layer)

Presentation-only families are removed from headless; every @vean/ui component name survives — only projects importing headless primitives directly are affected:

Removed headless sub-pathMigration
@vean/aria/cardSCard remains (now composed on CollapsibleRoot, open by default; recipe and ui override semantics unchanged)
@vean/aria/emptySEmpty remains (UI-only implementation)
@vean/aria/listNo S counterpart — plain lists use ul/li + UI classes; interactive lists use listbox / tree
@vean/aria/skeletonSSkeleton remains (UI-only implementation)
@vean/aria/badgeSBadge remains (UI-only implementation; dismissible via Button/Icon)
@vean/aria/tagSTag remains (UI-only implementation); interactive editable tags belong to tags-input

The Alert family stays in headless and gains the missing semantics: role?: 'alert' | 'status' (default 'alert'; the live region is implied by ARIA) — an addition, not a break.

7. SPopper / SArrow no longer exported from the UI package

The zero-style forwarders are no longer public — SPopper wrapped the headless PopperCompact plus a style recipe, and SArrow re-exported the headless Arrow. The styled surface is carried by popover / tooltip / hover-card:

- import { SPopper, SArrow } from '@vean/ui';
+ import { PopperCompact } from '@vean/aria/popper'; // unstyled assembly, positioning styles are on you
+ import { Arrow } from '@vean/aria/arrow';

8. Dependency changes

The headless runtime dependencies shrink to a whitelist where only engine-level capabilities may be third-party:

# Removed (they may have appeared in your lockfile as transitive deps)
@dnd-kit/dom            # PageTabs sorting now uses the self-built useSortableList
@dnd-kit/vue
@formkit/auto-animate   # animations now use the self-built collapseMotion state machine
@internationalized/date # see section 1
@internationalized/number
@standard-schema/spec   # validators come from your own Zod / Valibot

# Added
date-fns                # date engine
@tanstack/vue-table     # table engine
@tanstack/vue-form      # form engine

If your code imported any of the removed packages directly (relying on pnpm hoisting), declare them in your own package.json. For date-fns prefer sub-path imports (date-fns/addDays) so bundlers without sideEffects awareness only load what you use.

On tree-shaking: the bundle built from import { SButton } from '@vean/ui' is now asserted to contain no table / form / date-fns / embla code (the bundle fixture runs in CI), and the headless/ui runtime dependency whitelist is enforced by the pnpm check:deps gate.

9. SPagination: actionAsSelected → actionVariant

The flag that made the first/prev/next/last buttons adopt the item styling is renamed; semantics and the false default are unchanged, and the selected page keeps its variant styling either way:

- <SPagination variant="solid" action-as-selected :total="200" />
+ <SPagination variant="solid" action-variant :total="200" />

The styling props themselves (variant / shape / size) are untouched, and the recipe still applies the variant to the action buttons only while the flag is on. See the Pagination docs for the current prop table.

What you get in return

  • Rebuilt Drawer gesture kernel (Base UI model): CSS-variable-driven transforms, a snap point value model, a native touch pipeline (touch/scroll arbitration, scroll-edge swipe start), the DrawerIndent primitive and nested coordination.
  • Self-built motion: useSortableList for PageTabs sorting (FLIP swaps + keyboard reordering) and the collapseMotion height-collapse animation for form errors and tree expansion.
  • Table engine escape hatches: useTableEngine() injection, full tableOptions passthrough, and slot props carrying the engine instance.
  • Form expressiveness: dual-source generic inference for UseFormOptions (derive Values from defaultValues when no schema), field validate accepting Standard Schema objects and reactive rebinding.
  • Newly public exports: InputNumberClear, LayoutPlaceholder, MenubarMenus, NavMenuOptionCompact, TableCompactHead / TableCompactRow / TableVirtualSpacerRow.
  • A11y: the role="alert" | "status" semantics on Alert, with inert background marking (markOthers) carried over from the previous release.
  • CI quality gates: the tree-shaking bundle fixture and the dependency whitelist/blacklist check (pnpm check:deps).
  • The CLI moved to packages/cli; after the rebrand it ships as @vean/cli with the vean bin (see the brand guide below).

The rebrand ships with this release

The brand-level rename (SoybeanUI → Vean: the npm scope switch, @soybeanjs/headless → @vean/aria, data-soybean-* → data-vean-*, --soybean-* → --vean-*, the sbean CLI → @vean/cli) lands in the same window as the engine rebuild, so you migrate once. One command does the rewrite: npx @vean/cli@latest migrate rebrand --write; package mapping, runtime-contract renames, Nuxt/UnoCSS config and CLI steps live in the dedicated guide: Upgrade Guide: SoybeanUI → Vean.

Unchanged: the S component prefix (= Styled), design tokens --primary / --size / --radius, and @soybeanjs/cva / @soybeanjs/colord. The theme moved to a single localStorage envelope (__VEAN_THEME, see the theming guide) and the old __SOYBEAN_THEME* keys are no longer read.

Upgrade checklist

  1. pnpm remove @internationalized/date @internationalized/number && pnpm add date-fns; build date values with Date / createDate / createTime.
  2. Move table columns and state to the TanStack shapes (accessorKey / sorting / columnFilters / ExpandedState…); legacy fields have no aliases.
  3. Migrate useForm to the context-object return face and rename initialValues → defaultValues.
  4. SNavigationMenu → SNavMenu (and NavMenu* in headless).
  5. SDrawer → SSheet; SBottomSheet → SDrawer (v-model:active-snap-point → v-model:snap-point, scale background → DrawerIndent).
  6. Replace direct headless card / empty / list / skeleton / badge / tag imports with the UI components or your own composition.
  7. Replace SPopper with PopperCompact (or migrate to popover / tooltip / hover-card) and import SArrow replacements from @vean/aria/arrow.
  8. Declare any hoisting-relied removed packages explicitly in your package.json.
  9. SPagination’s actionAsSelected prop → actionVariant (:action-as-selected → :action-variant).
  10. After pnpm typecheck and a build passes, focus regression on date pickers, table sorting/filtering, form validation and drawer gestures.