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.x | v0.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-fnsThe 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' / minWidth | size: 140 / minSize (px numbers) |
group column { key, children } | { id, columns } |
sortState / defaultSortState | sorting / defaultSorting (SortingState) |
filterState / defaultFilterState | columnFilters / defaultColumnFilters (ColumnFiltersState) |
columnWidths / defaultColumnWidths | columnSizing / defaultColumnSizing (Record<string, number>) |
expanded: R[] | expanded: ExpandedState ({ '1': true } or true) |
update:sortState / update:filterState | update: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.4x | v0.50.0 |
|---|---|
useForm(options, Field, FieldArray) tuple | useForm(options) returns a context object (handleSubmit, SFormField, isSubmitting, form…) |
initialValues | defaultValues (TanStack naming) |
onSubmit(values, helper) | onSubmit(values) (FormSubmitHelper removed) |
formState.values / errors state snapshot | form.useSelector(state => …) / form.Subscribe (the raw form.state remains on FormApi) |
reValidateMode / field-level reset callback | Removed — set validateMode: 'change' for live validation; reset restores defaultValues |
Nested array path social.0.name | TanStack 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:
| Legacy | New |
|---|---|
SDrawer | SSheet |
drawerVariants | sheetVariants |
DrawerProps / DrawerEmits / DrawerSlots | SheetProps / SheetEmits / SheetSlots |
SBottomSheet (gesture drawer) → SDrawer — the family renames wholesale, and the kernel is rebuilt on the Base UI model:
| Legacy | New |
|---|---|
SBottomSheet / BottomSheetRoot / … | SDrawer / DrawerRoot / … (prefix maps one-to-one) |
@vean/aria/bottom-sheet | @vean/aria/drawer |
v-model:active-snap-point | v-model:snap-point |
direction prop (headless) | side prop |
data-vean-bottom-sheet-* | data-vean-drawer-* |
shouldScaleBackground / fadeFromIndex | Removed — 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-path | Migration |
|---|---|
@vean/aria/card | SCard remains (now composed on CollapsibleRoot, open by default; recipe and ui override semantics unchanged) |
@vean/aria/empty | SEmpty remains (UI-only implementation) |
@vean/aria/list | No S counterpart — plain lists use ul/li + UI classes; interactive lists use listbox / tree |
@vean/aria/skeleton | SSkeleton remains (UI-only implementation) |
@vean/aria/badge | SBadge remains (UI-only implementation; dismissible via Button/Icon) |
@vean/aria/tag | STag 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 engineIf 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
DrawerIndentprimitive and nested coordination. - Self-built motion:
useSortableListfor PageTabs sorting (FLIP swaps + keyboard reordering) and thecollapseMotionheight-collapse animation for form errors and tree expansion. - Table engine escape hatches:
useTableEngine()injection, fulltableOptionspassthrough, and slot props carrying the engine instance. - Form expressiveness: dual-source generic inference for
UseFormOptions(deriveValuesfromdefaultValueswhen no schema), fieldvalidateaccepting 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/cliwith theveanbin (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
pnpm remove @internationalized/date @internationalized/number && pnpm add date-fns; build date values withDate/createDate/createTime.- Move table columns and state to the TanStack shapes (
accessorKey/sorting/columnFilters/ExpandedState…); legacy fields have no aliases. - Migrate
useFormto the context-object return face and renameinitialValues→defaultValues. SNavigationMenu→SNavMenu(andNavMenu*in headless).SDrawer→SSheet;SBottomSheet→SDrawer(v-model:active-snap-point→v-model:snap-point, scale background →DrawerIndent).- Replace direct headless
card/empty/list/skeleton/badge/tagimports with the UI components or your own composition. - Replace
SPopperwithPopperCompact(or migrate to popover / tooltip / hover-card) and importSArrowreplacements from@vean/aria/arrow. - Declare any hoisting-relied removed packages explicitly in your
package.json. SPagination’sactionAsSelectedprop →actionVariant(:action-as-selected→:action-variant).- After
pnpm typecheckand a build passes, focus regression on date pickers, table sorting/filtering, form validation and drawer gestures.