Frappe UI v1 Changelog
User-facing v1 changes. Unreleased entries describe changes since v0.1.278. Log only breaking changes, deprecations, observable behavior changes, and migration guidance — not internal refactors or test additions.
All deprecations preserve backwards compatibility through v1.x and emit a one-time dev-mode warning (unless noted). Removal is post-v1.
Unreleased
Data fetching (v2) — stale responses no longer write the shared stores (fix)
Two concurrent writes to one document could leave docStore, listStore (and every view bound to them) on the response that settled last instead of the newest one, while data already held the fresh response (#1017).
The gate lives in the stores. Every request takes a dispatch version; the response's store writes carry it, and the stores reject a write for a document that a later-dispatched request has already written. One freshness domain covers every writer — the docs side channel, the useDoctype / useList / useDoc hooks, and any mix of instances or paths writing the same document. A version is recorded only when a mutating write lands: a newer request that failed wrote nothing on the server, so it does not make the older success stale, and a read (GET) is admitted on its version but records nothing — the server may answer a later reload before an earlier save commits, and the save must still land. A delete records a fresh version when it settles — a delete is terminal — so no in-flight write or reload, whatever its dispatch order, can re-create a deleted document. One accepted limitation: the gate orders by dispatch time, so a read dispatched after a save, handled by the server before the save committed and answered after it, is admitted and republishes the pre-save value — resolving that needs server-side sequencing, which this design trades away.
useAction still skips the onSuccess/onError hooks of a submit that a newer same-key submit of the same instance already outran — the store gate protects the stores, this skip only avoids re-running hook side effects with a stale response. useDoc's write members and useNewDoc skip their hooks the same way, but their store writes stay outside that skip: only the store gate decides them. It compares per document and knows whether the newer request landed, so an overtaken insert of a different document still lands, and an older success is kept when the newer submit failed.
No API change. Behavior changes if you relied on it:
- A stale
setValue/deleteon the same document no longer triggersuseList's auto-refetch; the newest submit's refetch already ran. - Submits with different keys, and keyless submits (inserts), are independent — all of their hooks still fire, as before.
Charts — a new family at frappe-ui/charts
A second chart family ships alongside the one at the package root. It is additive: the root chart exports keep working and nothing here removes them. Import the components from frappe-ui/charts, which carries the --chart-* color tokens with it. spec/charts.md states the conventions, and spec/adr/0015-what-enters-charts.md records what the family admits.
Landed so far:
- The chrome is exported —
ChartCard,ChartContainer,ChartLegendandChartTooltip— so a plot an app draws itself wears the family's look.ChartCardowns the card surface, andcard: falseturns it off. - Combo series through
seriesConfig[key].type, which also collapsed the three axis option builders into one. Per-series area fill falls out of it. - Reference lines on the axis charts and on
ScatterChart. A line beyond the data range is clipped rather than stretching the value axis. stacked: 'normalized'for shares, andmaxSeriesto cap a long grouping column.maxSerieshas no default.ScatterChartandSankeyChart.- Category labels fit themselves — the library measures, tilts and truncates instead of taking an angle prop.
xAxis.type: 'value'reads the x column as a quantity: a point sits at its own number instead of in its row's slot, and the rows draw in numeric order. Ask for it — a column of numbers still reads as categories by default.- The three states are slots —
#loading,#errorand#empty— onChartContainerand on every chart component, so an app puts a retry button beside a failed query without drawing chrome of its own.#loadingreplaces the whole placeholder rather than a caption under a spinner. - A loading chart draws a skeleton the size of its plot, where it used to draw a spinner and the words "Loading chart…". A dashboard fills in a card at a time, and a placeholder that holds the grid's shape reads better than eight spinners turning out of step.
#loadingtakes it back. seriesConfig[key].axisputs a series on the second value axis. It replaces they2prop, which is removed:ynames every series once, in the order they are drawn and colored, so a series no longer changes color when it changes axis. Long data reaches the second axis for the first time, keyed by a value of theseriescolumn, andy2Axisis unchanged. To migrate, move eachy2column intoyat the position it should draw at and addaxis: 'y2'to that column'sseriesConfigentry. TypeScript reports the removed prop, but a plain template passes it through as an attribute and draws the column not at all — grep fory2on the v2 charts after upgrading.ScatterCharttakesshowDataLabels, which prints each point'slabelbeside it. Names that collide with a neighbour are dropped.NumberCardtakescolor, the ink the reading is printed in, for a card standing for a series drawn in that color elsewhere. The card, the title and the delta tone are unchanged by it.
Changed since 1.0.0-beta.41, the first beta that shipped the family:
- Breaking, silent: the six mark emits are renamed to
select—datapointClickonAreaChart,BarChartandLineChart,sliceClickonDonutChart,stageClickonFunnelChart,cellClickonHeatmapChart,linkClickonSankeyChartandpointClickonScatterChart. P1 names an emit after the behavior, and the six now fire from the keyboard as well, so "click" was false. A listener on an old name stops firing with no error — see the migration guide. The payload types are unchanged. - Breaking, loud:
ChartThemeis nowChartTokensanduseChartThemeis nowuseChartTokens, which returns{ tokens }rather than{ theme }.thememeans a color tone everywhere else in the library (P4), and these are the resolved--chart-*values. - Breaking, loud:
formatValue,formatDate,formatLabel,formatPercent,formatAxisValue,currentColorSchemeandresolveChartThemeare no longer exported. They had no documented use, and each format helper hardcodesen-US. Read the plot-area colors withuseChartTokens, which re-resolves on a theme flip;currentColorSchemewas the rootresolvedColorSchemeunder another name.
Calendar family — moved to frappe-ui/experimental (breaking)
Calendar is not taken to bar at root for 1.0.0 (#1020, redirect of #989). It parks on frappe-ui/experimental (P14 — no stability promise) with its public API unchanged, until a redesigned calendar family replaces it.
- Breaking, loud:
import { Calendar, ... } from 'frappe-ui'fails to resolve. Import fromfrappe-ui/experimentalinstead:Calendar,CalendarColorMap,CalendarActiveEvent, and the typesCalendarActions,CalendarCellClickData,CalendarConfig,CalendarEvent,CalendarMode,CalendarPublicProps,CalendarTimeFormat,GroupedCalendarEvents. Migration is the import-path change only. Apps that spreadcontentfromfrappe-ui/tailwindkeep Calendar styles automatically. - Fix: the default header's month-title button renders again (it broke when DatePicker's
#targetslot became#trigger), and the all-day collapse buttons show their chevron icons again.
Radius aliases and text-*-black styles removed (breaking, silent)
Per ADR-0006 and ADR-0008 (#998, decided in #993):
- The named radius aliases (
rounded,rounded-sm,rounded-md,rounded-lg,rounded-xl,rounded-2xl, and their directional forms) are removed. Numbered tokens are the only radius vocabulary (rounded→rounded-4, sm→1, md→5, lg→6, xl→7, 2xl→8; identical px).rounded-noneandrounded-fullstay. Silent break: the preset replaces Tailwind's scale, so an unmigrated alias emits no CSS — square corners, no build error. Thetokens-v2codemod now performs these renames (idempotent, runs in every mode); it rewrites bareroundedonly inside quoted strings and@applyrules, so grep for leftovers. - The
text-<size>-black/text-p-<size>-blackstyle classes are removed (zero usage; the Figma black weights were corrupt export data). Also a silent break. The codemod flagsfont-extrabold/font-blacknext to a text size instead of merging onto the removed class.
ListFilter — removed (breaking)
- Breaking, loud:
ListFilteris no longer exported — the import fails. Its internals (SearchComplete,FilterIcon) are gone with it (#992, #999). No consumer app used it. Build filter UI in app code withSelectandCombobox.
Data fetching (v2) — useDoc writes and useNewDoc get one request per submit (fix)
useDoc's setValue, delete and every methods: entry, and useNewDoc, still held a single shared request after the useDoctype/useList fix. Two submits at once aborted one another, and every submit resolved from the same data, so a caller could receive another caller's answer or null. Each submit now sends its own request and resolves with its own response (#991).
- No API change. These members keep the full
useCallsurface — same members, same types.submit()still resolvesnullon a failed request. dataanderrorbelong to the submit that started last, same asuseDoctypeanduseList: a stale submit answers its own caller and writes nothing shared.loadingstaystrueuntil every submit settles.- Behavior change if you relied on it: a second submit no longer cancels the first — both requests reach the server.
Sprite icon trio — moved to frappe-ui/experimental (breaking)
The sprite-based Icon, IconPicker, and spritePlugin leave frappe-ui/icons (#904). Apps still use them, so they park on frappe-ui/experimental (P14 — no stability promise) instead of being deleted. lucide-* classes are the canonical way to render icons. The named SFC icons (CircleCheckIcon, HelpIcon, ...) stay on frappe-ui/icons.
- Breaking, loud:
import { Icon, IconPicker, spritePlugin } from 'frappe-ui/icons'fails to resolve. Import fromfrappe-ui/experimentalinstead. Migration is the import-path change only. Apps that spreadcontentfromfrappe-ui/tailwindkeepIconPickerstyles automatically — no Tailwind change needed. frappe-ui/experimentalexportsIcon(sprite); rootfrappe-uiexports a differentIcon. Alias one if you import both:import { Icon as SpriteIcon } from 'frappe-ui/experimental'.
createListResource — hasPreviousPage stale after reload() (fix)
reload() temporarily resets start to 0 to re-fetch the accumulated pages as one request, then restores it. hasPreviousPage was computed while start was still 0 and never recomputed after the restore, so it stayed false even when start was back above 0. reload() now recomputes hasPreviousPage after restoring start.
CommandPalette — show renamed to open (breaking, silent)
show → open, matching the rest of the library's overlay vocabulary (CONTEXT.md). Silent break: Vue accepts the unknown show prop with no error, so the palette just never opens — grep for CommandPalette and v-model:show after upgrading. Mod+K now opens the palette on its own (registered internally via useShortcut); delete any app-level keydown listener that toggled it. CommandPalette and CommandPaletteItem also gained types.ts, docs, stories, and a Cypress test; item icons now accept string | Component (lucide class strings, emoji, or a component) instead of components only.
KeyboardShortcut — deprecated shortcut and unused modifier props removed (breaking)
Per ADR-0008, the deprecated shortcut prop (superseded by combo) is removed rather than shipped frozen — it had zero call sites. The meta / ctrl / shift / alt boolean props are also removed (Rule 9: zero real usage, superseded by combo). Both are loud breaks (removed props on a typed component). Also moved from a bare KeyboardShortcut.vue into its own directory with a types.ts, a docs page, and a Cypress test.
useShortcut — matchesShortcut no longer public (breaking)
matchesShortcut is removed from the frappe-ui package export. Its own doc comment already said "exported for unit tests only" — it was never meant to be public API (Rule 9). Loud break (import error) for anyone who imported it directly; no signal of any real consumer doing so.
KeyboardShortcutsModal / useShortcut — brought to bar
KeyboardShortcutsModal gained a types.ts and a Cypress test (previously only unit-tested). useShortcut gained a short entry on the composables page; its API was already stable and is unchanged.
SettingsDialog — SettingsBody's exposed type
SettingsBody's viewportElement expose now has a typed SettingsBodyExposed type (ADR-0012), exported from frappe-ui. No behavior change.
FileUploader — uploads default to private (security fix, breaking)
- Silent break:
isPrivateUpload()— the single resolver used byuseFileUploadandFileUploadHandler— now defaults an upload with no statedprivate/is_privateto private, not public. PreviouslyFileUploaderpatched this default at the component level whileuseFileUpload().upload(file, {})andnew FileUploadHandler().upload(file, {})resolved the same missing intent to public. The two disagreeing was #922;FileUploaderitself already uploaded private (sincev1.0.0-beta.21) and is unaffected. A caller ofuseFileUploadorFileUploadHandlerwith no explicit privacy option now gets a private file where it previously got a public one. See the migration guide. - Fixed: the standalone
upload(file, options)export (re-exported fromfrappe-uialongsideuseFileUpload) crashed at runtime — it required internalstate/resetarguments the public signature never exposed a way to pass. It's now a real standalone function;useFileUpload()wraps it with reactive state.
FileUploader — flat props replace the uploadArgs blob (breaking, P3)
- Silent break:
uploadArgsis removed. Its fields that are actually used in the wild are now flat props:private,folder,doctype,docname,fieldname,uploadEndpoint,optimize. Old code keeps compiling —uploadArgsbecomes an inert attribute on the root element — so an app that relied on it (folder,doctype, customprivate, …) silently stops applying those options. Advanced options with no flat prop (file_url,method,type,params,maxWidth/maxHeight, upload cancellation) had zero measured use on the component (rule 9); reach foruseFileUpload()directly for those. See the migration guide.
FileUploader — success / failure emits declared stable
- Both emits lost their
@deprecatedtag (ADR-0008 forbids shipping@deprecatedmembers at1.0.0) and gained real types:success: [data: UploadedFile],failure: [error: unknown](previously untypedany). Both are load-bearing at real call sites and keep their names — they already read as behaviors (P1), not interactions. - Fixed:
failuredidn't fire whenvalidateFilerejected a file — only on an actual upload error.errorwas set on the slot props either way, but a listener on@failurenever heard about a validation rejection. It now emitsfailurewith the validation error (string orError) in both cases, matching what the type already promised.
FileUploader — slot prop error is now always a string
- Silent break:
FileUploaderSlotProps.errorchanged fromunknowntostring | null. Upload failures were already normalized to a message string; validation failures (validateFilereturning anError) were not — a custom slot could receive either a string or anErrorobject. Both paths now normalize to a message string before reaching the slot. A slot that did{{ error.message }}expecting the validation-Errorcase (uncommon, but not impossible) silently renders nothing now thaterroris always a string. See the migration guide.
FileUploader — inputRef removed, nothing in its place
- Breaking, loud: per ADR-0012,
FileUploaderhands back nothing through a template ref.inputRefwas a function disguised as a ref (uploader.value.inputRef().focus()), and theopenFileSelectorslot prop already covers what it was used for. Zero known call sites.
FileUploader — structural bar: TypeScript, types.ts, tests, docs
FileUploaderis now fully typed (FileUploaderProps,FileUploaderEmits,FileUploaderSlotPropsintypes.ts), has*.cy.tscomponent tests covering the five at-bar behaviors, and adata-slot="root"/data-state="idle" | "uploading" | "success" | "error"pair for CSS hooks (P10). The default fallback trigger now renders validation/upload errors via<ErrorMessage>(role="alert") — previously invisible unless the caller used a custom slot.
fileToBase64 and the fileSize helpers — unexported from root
- Breaking, loud:
fileToBase64,formatBytes,getMaxFileSize, andfileSizeLimitMessageare no longer exported fromfrappe-ui. Zero external call sites at the v1 sweep (rule 9) — all four stay as internal helpers shared byuseFileUpload,FileUploadHandler, and the editor's media upload engine.
Sidebar — deprecated config API removed (breaking)
Per ADR-0008, every member marked @deprecated is deleted. Sidebar is now a bare composable frame: SidebarHeader / SidebarSection / SidebarLabel / SidebarItem compose in the default slot, matching the direction agreed in v1-release/plan.md.
- Breaking, silent:
Sidebar'sheaderandsectionsconfig-object props are gone. Old code still compiles — Vue drops them as inert attrs — but the sidebar renders empty instead of the configured header/sections. ComposeSidebarHeaderandSidebarLabel+SidebarItem(orSidebarSection) directly in the default slot. - Breaking, silent:
Sidebar's#header-logoand#footer-itemsslots are gone (they only existed to reach into the config-object layout). Old<template #header-logo>/#footer-items>content stops rendering. Put that markup directly in the default slot instead. - Breaking, silent:
SidebarSection'sitemsprop and#sidebar-itemscoped slot are gone. It's now a plain collapsible-group wrapper —label,collapsible,v-model:collapsed— whose children areSidebarItems composed directly in its default slot, instead of anitemsarray plus a slot to customize each row. - Breaking, silent:
SidebarItemProps.isActive(alias foractive) and.condition(config-object visibility filter) are gone. Useactive; usev-ifon the composedSidebarIteminstead ofcondition. - Breaking, silent:
SidebarHeader's#logoslot is renamed to#prefix(P6 — no type-specific slot names when a generic one covers them). Old<template #logo>content stops rendering; the default logo/initial box shows instead. SidebarItem's collapsed-rail icon no longer swaps to a centered square and back while the sidebar's width animates — it holds one position through the transition (also fixes the icon sitting 2px off the rail's center line).SidebarSection's collapsible label is now a real<button>witharia-expanded/aria-controls, keyboard-operable (was a<div>with a click handler and no keyboard path).
ListView family — moved to frappe-ui/experimental (breaking)
ListView is not taken to bar at root for 1.0.0. frappe-ui/list is the recommended primitive for new code, but it's a narrower, composition-based family by design — it has no equivalent for ListView's config-driven columns (resizable widths, per-column getLabel/prefix functions, cell tooltips, disabled-row exclusion, the built-in select banner). Rather than freeze the whole 12-export barrel at root undeprecated, it moves to frappe-ui/experimental (P14 — no stability promise) and stays there until frappe-ui/list reaches full functional parity.
- Breaking, loud:
import { ListView, ... } from 'frappe-ui'fails to resolve. Import fromfrappe-ui/experimentalinstead:List,ListView,ListEmptyState,ListFooter,ListGroupHeader,ListGroupRows,ListGroups,ListHeader,ListHeaderItem,ListRow,ListRowItem,ListRows,ListSelectBanner.
TextEditor and its v0 exports — removed from root (breaking)
Per ADR-0008, the deprecated v0 editor exports are removed from top-level frappe-ui — loud breaks, the import fails to resolve:
TextEditor,TextEditorBubbleMenu,TextEditorFixedMenu,TextEditorFloatingMenu,TextEditorContent,createEditorButtonImageExtension,SetImageOptions,createSuggestionExtension,BaseSuggestionItem,CreateSuggestionExtensionOptions(the twoTextEditor/extensions/*barrels also re-exported from root)
Use Editor and its kits/building blocks from the frappe-ui/editor subpath instead — see the migration guide's Editor section. This confirms CONTEXT.md's rule: the editor family is the only subsystem that exports from a subpath rather than root, and nothing editor-related is exported from root anymore.
The underlying v0 component files still ship, unmodified, as frappe-ui/editor's migration safety net. They are parked in frappe-ui/experimental (experimental/TextEditor/, #1007), so apps mid-migration keep an import path:
import { TextEditor } from 'frappe-ui/experimental'This path is unstable — no deprecation window. Sharing the experimental barrel costs its other importers nothing in production: #870's rollup measurement shows unused re-export chains are pruned before sideEffects marking applies, so the editor graph is tree-shaken out of non-editor imports. Removing the files is a separate, human-gated cleanup once every consumer has migrated (spec/editor.md §12); the TextEditor public API redesign itself is out of scope for 1.0.0 and carved out to 1.1.
Editor and TextEditor styles — Tailwind v4 theme() call fixed
.ProseMirror ul[data-type='taskList'] input[type='checkbox'] used a Tailwind-v3-only theme('colors.gray.900') call in both frappe-ui/editor's and the v0 TextEditor's stylesheet, which broke Tailwind v4 builds (#861 — a remaining instance of #299). Replaced with the same var(--ink-gray-9) token the rest of both files already use.
v1 resources — at-bar exception documented; listResource gets test coverage
v1 resources (createResource, createListResource, createDocumentResource, getCachedResource, getCachedListResource, getCachedDocumentResource, resourcesPlugin, saveLocal, getLocal, deleteLocal, onDocUpdate) ship un-deprecated and frozen at 1.0.0, per #886. ADR-0013 records the one exception: the implementation stays hand-written JavaScript rather than TypeScript, permanently — 344 production call sites make a rewrite riskier than the freeze. createListResource, the second-most-used export at 57 call sites, gets test coverage for the first time (listResource.test.ts): pagination, insert/setValue refreshing the list, caching, and reload()'s pagination-state restore.
Tailwind preset — content export added
frappe-ui/tailwind exports content, the glob list of frappe-ui source directories that emit Tailwind classes. Spread it into your app's tailwind.config.js content array instead of hand-maintaining the paths — see the new Tailwind Setup docs page. Tailwind v3 doesn't merge a preset's content, so this was previously unavoidable hand-maintenance, and it had already drifted: some apps on [email protected] glob src/components/** only, silently dropping every class the editor and list molecules emit.
Tailwind preset — tokens.js export removed (breaking)
The ./tailwind/tokens.js export is removed outright, with no deprecation window. It had zero importers anywhere and re-exported colorPalette.js via export *, the implementation-module re-export pattern disallowed by P15. Use the preset (frappe-ui/tailwind) directly.
This ships before the 1.0.0 tag, while the library "evolves freely" (P13) — the freeze that requires a deprecation window starts at the tag, not before it. Zero call sites is also why it's a same-release removal rather than a carried-forward deprecation: there is no consumer for a warning to reach.
Tailwind preset — unused token vocabulary and utilities removed (breaking)
Design-token audit before the additive-only freeze (#940): every family in tailwind/generated/*.json and every utility/--* variable plugin.js emits was checked against frappe-ui's own source, docs, and stories, plus a fresh census of all consumer apps (crm, helpdesk, gameplan, insights, builder, suite, central, frappe_calendar, frappe-ui-starter, and frappe's ui/ package). The primitive and semantic color ramps (all twelve hues, in surface-*/ink-*/outline-*), and every typography weight (including bold/black) and size through text-12xl, turned out to be real, in-use vocabulary — none of that is touched. What had zero call sites everywhere is removed:
text-tinyand its uppercase text-transform. Not even shown in the docs' own type-scale page.text-13xlthroughtext-16xl(and their-medium/-semibold/-bold/-blackvariants). The docs' own "display sizes" showcase stops attext-12xl— these four sizes were past what even the type-scale demo used.shadow-statusand its backing--elevation-statusvariable. Named once in prose on the elevation docs page but never rendered there or anywhere else.surface-alert-button-*/ink-alert-button-*(default,info,success,warning,error).Alert's buttons color via the sharedvariant+themeaxes (P4); this Figma spec never got wired to code.surface-alpha-gray-2-overlay. Resolved to the black/white overlay ramp rather than the gray-alpha ramp its name implies, breaking the{family}-{step}pattern every othersurface-alphaentry follows.
All five are silent breaks — a missing Tailwind class or --* var just stops applying, no build or type error. See the migration guide.
The token generator (tailwind/figma-tokens-to-theme.js) now filters these out at the source, so they stay gone on the next yarn sync-tokens run rather than reappearing. ALPHA_FAMILIES also dropped a dead 'red-alpha' entry that never matched anything in the Figma export — no emitted token changed.
Also removed: tailwind/colors.js, a 642-line legacy color module superseded by colors.json + colorPalette.js. It had zero importers and wasn't reachable through any frappe-ui package export (no ./tailwind/* wildcard) — deleting it doesn't change anything for consumers.
frappe-ui/vite — types and docs
frappe-ui/vite now ships hand-written types (vite/index.d.ts, wired via the types export condition), so frappeui(...) and its options (frontendRoute, lucideIcons, barrelImports, frappeProxy, jinjaBootData, buildConfig, frappeTypes) are typed without a // @ts-expect-error workaround. Also added a docs page covering every sub-plugin, including barrelImports — previously undocumented on the docs site.
list-style.css and editor-style.css exports — removed
- Breaking: the manual
frappe-ui/list-style.cssandfrappe-ui/editor-style.cssexports are gone (loud — the consumer build fails withMissing "./list-style.css" specifier). They existed only because bundlers tree-shook the side-effectimport './style.css'inside thefrappe-ui/listandfrappe-ui/editorbarrels. The barrels are now listed insideEffects, so each family's CSS ships automatically the moment you import anything from its subpath — delete the manual@importlines. The tree-shake was never Rolldown-specific: plain Rollup/Vite production builds dropped the CSS too.
Toggles and ranged inputs — deprecated members removed
Per ADR-0008, the family's deprecated aliases are removed, not shipped frozen. All five had zero call sites across the consumer apps. These are silent breaks — old code compiles and runs, but the name is ignored (see the migration guide's Inputs table):
Rating.rating_from→max;Rating.readonly→disabled.Switch.changeemit →v-model/@update:modelValue;Switch.labelClasses→data-*styling hooks.Checkbox.padding→padded.
Slider now exports its types (SliderProps, SliderEmits, SliderValue), and Rating exports RatingEmits.
CircularProgressBar — removed
- Breaking:
CircularProgressBaris no longer exported (loud — the import fails). It was a second component forProgress's concept (P8), with hardcoded light-mode colors and a structuredthemeobject prop (P3/P4). One call site existed across all consumer apps. UseProgress, or copy the old SFC into your app if you need the radial form.
Node.js requirement
- Breaking: Node floor is now
>=20.19.0(was Node 18 on 0.1.x). Declared viapackage.jsonenginesso installers and CI surface the requirement instead of opaque transitive-dep engine errors.
Portal target for embedded apps
portalToonPopover,HoverCard,Dropdown,Select,ComboboxandMultiSelectno longer declares a'body'prop default. An unembedded app still gets'body', now as a fallback. No existing call behaves differently.- New
usePortalTarget/providePortalTarget/portalTargetKeyexports let an embedding host redirect every overlay at once. Seespec/portal-target.md.
Dialog — v1 spec
- Flat top-level props (
title,message,icon,size,position,paddingTop,actions) are canonical. The legacyoptionsblob is removed — see below. v-model:openis canonical;v-model(modelValue) still works silently.- New props:
dismissible(defaulttrue, replacesdisableOutsideClickToClose),bare,showCloseButton(defaulttrue, independent of the auto-header). - Canonical slots
#default,#title,#actions(scoped with{ close, actions }). The legacy#body*slots are removed — see below. icon.theme(yellow | blue | red | green) replacesicon.appearance, which is removed — see below.- Auto-header no longer renders an "Untitled" fallback.
Dialog — imperative dialog.* API
- New callback-based helpers:
dialog.confirm(),dialog.danger(),dialog.prompt(). TheonConfirmcallback runs on click; resolving auto-closes the dialog, while throwing keeps it open and renders the thrown message inline. The action button shows a loading state untilonConfirmsettles. Each helper also returns a synchronous handle withclose()for programmatic dismissal. <FrappeUIProvider>now renders<Dialogs />next to<Toasts />, so apps wrapped with the provider get the imperative stack for free.<Dialogs />is still exported for callers that don't use the provider.ConfirmDialogandconfirmDialog()are removed — see below; usedialog.confirm()/dialog.danger().- New root exports:
DangerArgs,DialogControl,PromptControl,DialogHandle,PromptFieldValidator.DialogSlotPropsis exported from theDialogbarrel.
Dialog — deprecated surface removed (breaking)
Every member marked @deprecated is deleted, per ADR-0008. Nothing is aliased and nothing warns.
- Breaking, silent: the
optionsblob prop andDialogOptionstype are gone. It bundledtitle/size/icon/actionsinto one object. An:options="{...}"call site still compiles — Vue drops the unknown prop as an inert attr — but the dialog silently loses its title, size and actions. Use the flat top-level props. - Breaking, silent:
disableOutsideClickToCloseis gone. It still lands as an inert attr, anddismissible(the inverse) defaults totrue, so the dialog silently becomes dismissible. Usedismissible. - Breaking, silent:
icon.appearanceandDialogIconAppearanceare gone; onlyicon.themeremains. Anappearancekey is dropped, so the icon renders with no tone. Mapwarning → yellow,info → blue,danger → red,success → green. - Breaking, silent: the legacy
#body,#body-content,#body-main,#body-titleand#body-headerslots are gone. Vue drops an unknown named slot with no error, so a missed call site renders nothing where that slot's content used to be. Use#default,#titleand#actions;#bodymaps tobare+#default. - Breaking: the callable-context shim on action
onClickis gone. The context used to be callable as well as a plain object (ctx()closed the dialog); it is{ close }only now, so calling it as a function throwsTypeError: ctx is not a function. - Breaking:
defineExpose({ close })and theDialogExposedtype are gone — Dialog exposes nothing on its template ref (ADR-0012). A template-ref.close()call throws aTypeError. Usev-model:open = false, or thecloseslot prop. Zero known call sites. - Breaking:
ConfirmDialogandconfirmDialog()are deleted. The import fails, so the build names every call site. Usedialog.confirm()/dialog.danger().
Before/after for the silent breaks is in the migration guide.
DatePicker family — v1 spec
DatePicker, DateRangePicker, and DateTimePicker share the v1 popover-trigger vocabulary used by Combobox / Dropdown / Select.
side(default'bottom') +align(default'start') +offset(default4) replaceplacement(removed).keepOpen(defaultfalse) replacesautoClose(removed, inverse).typeable(defaulttrue) replaces picker-levelreadonlyandallowCustom(both removed).:typeable="false"blocks typing while keeping the popover interactive.- Constraints:
min?: stringandmax?: string(YYYY-MM-DD, plusYYYY-MM-DD HH:mm:ssonDateTimePicker), andisDateUnavailable?: (date: Dayjs) => booleanfor arbitrary disabling. Min/max and the predicate compose. OnDateTimePicker,minDateTime/maxDateTimeare removed in favor ofmin/max. v-model:opensupported on all three pickers viaopen+update:open.openOnFocus(defaultfalse) andopenOnClick(defaulttrue) let consumers opt out of either trigger path. Same defaults applied toComboboxfor parity.#triggeris the canonical custom-trigger slot;#targetis removed.DateRangePicker.clearablenow defaults totrue; footer hides when there is nothing to clear. Live hover preview while picking the end date and a stable trigger width derived fromformatwere added in the same pass.- Public type exports added:
DateTimePickerProps,DateRangePickerEmits,DateTimePickerEmits,DateRangeValue.
DatePicker family — DateRangePicker emit shape (breaking)
DateRangePicker emits update:modelValue / change as a [from, to] tuple (DateRangeValue = [string, string] | []) instead of a comma-joined string. The modelValue prop already accepted string[]; the emit is what changes.
// before
function onChange(v: string) { const [from, to] = v.split(',') }
// after
function onChange(v: DateRangeValue) { const [from, to] = v } // [] when clearedReactive forms that pass the value through unchanged are unaffected.
DatePicker family — footer removed; new #actions sidebar slot
The dedicated popover footer on DatePicker, DateRangePicker, and DateTimePicker has been removed, including the auto-rendered Clear button that used to render there when clearable && hasValue. clearable still governs the input-level clear affordance.
- New
#actionsslot renders as a left sidebar inside the popover. Slot props includeclose,setDate/setRange, andclear.DateRangePicker'ssetRange([from, to])commits both endpoints atomically — use it for fixed-window presets ("Last 7 days"). - Popover content is
w-fitwhen the slot is provided. data-slot="actions"is set on the sidebar<aside>for CSS hooks.
Migration: callers who relied on the auto-rendered Clear button should render an explicit Clear row inside #actions using the clear slot prop.
DateTimePicker — date selection keeps popover open (breaking)
Selecting a date in DateTimePicker no longer auto-closes the popover. Focus moves into the embedded TimePicker instead, so users get a continuous date → time flow. The popover closes on Esc, click-outside, or programmatic close().
Migration: callers relying on the implicit close should bind v-model:open and close from @update:modelValue, or render an "Apply" button in #actions (which receives close in its slot scope).
TimePicker — v1 refresh
Same vocabulary as the DatePicker family plus a flexible parser.
side/align/offsetreplaceplacement(removed).keepOpen(defaultfalse) replacesautoClose(removed).typeable(defaulttrue) replaces picker-levelreadonly/allowCustom(both removed).v-model:openviaopen+update:open; newopenOnFocus(defaultfalse) andopenOnClick(defaulttrue) props.- Flexible typed input:
"3pm","3.30pm","1500","9:30:15 am"parse to canonicalHH:mm[:ss]. min/maxreplaceminTime/maxTime(removed).scrollModeis removed; list is always centered on the selection.- Template ref exposes only
focus()(ADR-0012).selectAll()andblurInput(), dead members with no callers, are removed.
DatePicker family — keyboard navigation
Full keyboard nav inside the calendar grid (WAI-ARIA APG Date Picker Dialog spec).
↓on the trigger input opens the popover and moves focus to the selected/today cell.- Grid:
←/→±1 day,↑/↓±1 week,Home/Endweek edges,PageUp/PageDown±1 month,Shift+PageUp/Shift+PageDown±1 year. Enter/Spaceselects.Esccloses and returns focus to the input.- Disabled dates (via
min/max/isDateUnavailable) are skipped. - Arrow keys auto-advance across month boundaries.
DateRangePickerdual-pane: arrow keys cross panes without advancing the view; range-hover shading tracks the keyboard-focused cell.
Roving tabindex: exactly one cell is in the tab order, so Tab enters and leaves the grid as a single unit. Custom #trigger slots opt in automatically — any open path moves focus into the grid since a non-TextInput trigger has no typing context.
DatePicker family — legacy composable removed
useDatePicker and its helpers (getDate, getDatesAfter, getDaysInMonth, isLeapYear) were not used by any picker component and were not part of the v1 API. Deleted outright — the import fails, so the break is loud.
DatePicker / TimePicker family — deprecated aliases removed (ADR-0008)
The back-compat aliases these components carried through the betas are deleted, not kept as warn-and-map shims — per ADR-0008, no deprecated member ships in 1.0.0. Before/afters in the migration guide.
placement,autoClose,allowCustom, picker-levelreadonly,inputClass,valueprop removed. All silent: a leftover prop lands as an inert extra attribute instead of doing anything.#targetslot removed. Content in a leftover<template #target>silently stops rendering. Use#trigger.DateTimePicker.minDateTime/maxDateTimeandTimePicker.minTime/maxTimeremoved. Silent: the constraint just stops being enforced. Usemin/max.TimePicker.scrollModeremoved. Silent; the list is always centered.
change stays as a supported second emit alongside update:modelValue — it was never deprecated on TimePicker, and DateTimePicker depends on it internally, so removing it from the other two pickers would have been an inconsistent, unforced break.
Input family — shared labeling contract
TextInput, Textarea, Password, Checkbox, Switch, Rating, and Slider accept label, description, error, required. Id is auto-generated; <label for>, aria-describedby, aria-errormessage, aria-invalid, aria-required are wired automatically. error accepts string or Error (with Error.messages rendered as stacked plain text). Existing call sites unchanged.
Input family — data-* styling hooks
Every input shell renders the canonical data-* vocabulary so external CSS can target inputs without class-injection props:
data-slot("label","control","description","error")data-size,data-variant(where applicable)data-state("valid" | "invalid" | "checked" | "unchecked" | …)data-disabled,data-required
Password — v-model fix
Password now uses defineModel<string>(), fixing the existing bug where <Password v-model> did not update from typing. Explicit size, variant, disabled, placeholder, id, required props replace $attrs routing.
Password — value prop removed (breaking)
Per ADR-0008, no deprecated member ships in 1.0.0. value warned and seeded v-model since it was deprecated earlier in this cycle; a census of every downstream app found zero call sites still passing it. Use v-model / modelValue.
TextInput / Textarea / Password / Duration — focus() and inputElement on the ref
Implements ADR-0012.
- Breaking:
TextInput.elandTextarea.elare renamed toinputElement— a computed, typedHTMLInputElement | null/HTMLTextAreaElement | null, never a raw ref. - All three, plus
Duration, now exposefocus(options?: FocusOptions).Passwordpreviously exposed nothing. TextInput,Textarea, andPasswordshare one exported type,TextInputExposed, fromTextInput'stypes.ts.DurationExposed.focusgained the sameoptions?parameter; its member set is unchanged.
Rating — max replaces rating_from
Default 5. (The old name was kept as a deprecated alias during the betas and is now removed — see "Toggles and ranged inputs" above.) Rating no longer imports FeatherIcon; default star comes from lucide-star via the shared Tailwind plugin. Filled stars now render visibly for non-zero values.
Slider — additive props and a11y fix
disabledprop added.size: 'sm' | 'md'added;'md'scales track and thumb proportionally.- New
value-commitemit fires when dragging ends (use for side-effects you don't want on every step). - Removed hardcoded
aria-label="Volume". Labeling now flows through the shared contract; passlabelexplicitly. (Treated as a bug fix — every non-volume call site was announced as "Volume" by assistive tech.) - Visibility: visible track in collapsed wrappers, full-width by default.
- Uncontrolled
Sliderinitializes tomininstead of rendering with no thumb.
Switch — Lucide icons; deprecations
No longer imports FeatherIcon. icon is now string | Component; lucide-* strings route through the shared Tailwind plugin. labelClasses and the change emit were deprecated during the betas and are now removed (see "Toggles and ranged inputs" above). Row hover/active background removed.
Checkbox — padding deprecated
In favor of padded. (Now removed — see "Toggles and ranged inputs" above.)
Textarea — ghost variant; required prop
Textarea now accepts the 'ghost' variant (matching TextInput and Password) and the shared required prop.
TextInput / Textarea — ghost variant paints transparent (fix)
ghost set no bg-* class, so @tailwindcss/forms preflight painted the input #fff — a white pill in dark mode. ghost now sets bg-transparent, matching Combobox's own ghost search input. Closes #851.
FeatherIcon — removed (breaking)
Per ADR-0008, the deprecated FeatherIcon component is deleted, along with the feather-icons dependency. lucide-* strings (or a Component) are the only supported icon forms now.
- Breaking, loud:
import { FeatherIcon } from 'frappe-ui'and<FeatherIcon>fail at the import. - Breaking, silent: every icon-name prop across the library (
Button.icon/iconLeft/iconRight,Dialog.icon,Dropdown/ContextMenuitemicon,TabButtonsoptionsicon/iconLeft/iconRight,Icon.name) used to fall back toFeatherIconfor a bare feather-style name (e.g."edit"). That fallback is gone: an unrecognized string now renders nothing, with a dev-mode console warning once per (component, prop). Prefix the name withlucide-.
<!-- before -->
<FeatherIcon name="plus" class="size-4" />
<Button icon="plus" />
<!-- after -->
<span class="lucide-plus size-4" aria-hidden="true" />
<Button icon="lucide-plus" />Hardcoded internal FeatherIcon usages across core components were migrated to lucide-* in this release.
Before/after for the silent break is in the migration guide.
Input — removed (breaking)
- Breaking:
Inputand itsInput.cy.tstests are deleted. Per ADR-0008, no deprecated member ships in1.0.0; a census of downstream apps found no live call sites left that render<Input>(five registrations were dead global component registrations, never rendered). UseTextInputfor text-like modes, orTextarea/Select/Checkboxfor the other type modesInputaccepted.
Card, ListItem, standalone Toast — removed (breaking)
Per ADR-0008, three unmaintained wrappers that shipped @deprecated in code are deleted, not carried forward. All three had zero call sites across the census of downstream apps.
- Breaking:
Cardand its.vuefile are removed. No drop-in replacement — rebuild the title/subtitle/actions/loading layout with plain markup, usingLoadingTextorSkeletonfor the loading state. - Breaking:
ListItemand its.vuefile are removed. No drop-in replacement — rebuild the title/subtitle/actions row with plain markup. - Breaking: the standalone
ToastSFC (import { Toast } from 'frappe-ui') is removed. This only affects direct usage of the rawToastRoot-based component; the imperative API (toast()/toast.success()/toast.error()/toast.info()) and<ToastProvider>are unaffected and unchanged.
All three fail loudly at the import. Before/after examples are in the migration guide.
FormLabel — moved to a component directory (non-breaking)
FormLabel now lives at src/components/FormLabel/FormLabel.vue instead of a bare src/components/FormLabel.vue, matching the rest of the input family. It gains types.ts, tests, stories, and a docs page. The import path for consumers (import { FormLabel } from 'frappe-ui') is unchanged.
LoadingIndicator / LoadingText — moved to component directories (non-breaking)
Same move as FormLabel, for the same reason: both now live at src/components/LoadingIndicator/ and src/components/LoadingText/ instead of bare .vue files directly under src/components/. Each gains types.ts (LoadingIndicatorProps, LoadingTextProps), stories, a docs page, and cypress tests. The import path for consumers (import { LoadingIndicator, LoadingText } from 'frappe-ui') is unchanged.
Kept as distinct components from Spinner and Skeleton (P8) — usage data across the consumer census shows real, separate demand: LoadingIndicator (~60 files) and LoadingText (~11 files) are both load-bearing, not redundant overlap.
Icon — docs page and stories added
Icon had no stories/ folder, so it did not appear in the docs site despite being a public export. It now has a docs page and two stories (lucide string form, and the Component escape hatch).
MonthPicker — removed (breaking)
MonthPicker and its whole barrel (MonthPicker.vue, types, stories) are deleted. It duplicated Select for a narrower case. Use Select with month options — see the migration guide. The import fails, so the break is loud.
Legacy components — dev-mode warnings
Pill is no longer exported from the package entrypoint. It remains an internal TabButtons detail.
ThemeSwitcher remains exported for v1 compatibility, but is deprecated. For new theme switchers, compose Select with the useColorScheme composable.
Autocomplete — removed (breaking)
- Breaking:
Autocompleteand itsAutocompletePropstype are deleted. UseComboboxfor one value andMultiSelectfor several. The import fails, so the build names every call site.trigger="button"on either replacement is the shapeAutocomplete's default target had: a button showing the selection, with the search box inside the popover. - Breaking, silent:
FormControl type="autocomplete"is removed. The dispatcher falls through toTextInputand still forwards the type, so the result is<input type="autocomplete">— a plain text box, with no build or runtime error. A dev-onlyconsole.errornames the removal. - Breaking, silent: the
v-modelpayload inverts.Autocompletemodelled the whole option object; both replacements model the value only. Listen to@update:selectedOptionwhere the whole option is needed. - Breaking, silent:
#target'sopenslot prop was the function that opened the popover;#trigger'sopenis the open state. Anything reading it as a value (v-if="open") was always truthy and now is not. #target→#triggerotherwise:ComboboxandMultiSelectattach the open toggle to the trigger element themselves, so drop the click handler. AtogglePopover()carried through the rename throws on click — the popover still opens, so it reads as working while logging an error.- Grouped options use
{ group, options }, not{ group, items }. Both normalizers now throw naming the group and the rename, rather than dying inside amapcall.
Before/after for each silent break is in the migration guide.
Dropdown / ContextMenu — deprecated members removed (ADR-0008)
Three surfaces that shipped as deprecated aliases in the betas are deleted, not aliased. All three are silent breaks in plain-JS apps — before/afters in the migration guide; TypeScript callers get compile errors (the removed keys stay typed as never), and a dev-mode console warning fires when the old shape reaches the menu at runtime.
placementprop andDropdownPlacementtype removed. Usealign(left→start,center→center,right→end). A leftoverplacementis ignored and the menu falls back toalign="start".{ group, items }removed. Use{ group, options }, matchingCombobox/MultiSelect/Select. A leftoveritemsgroup renders as an empty menu.component:option rows removed (DropdownComponentOption,ContextMenuComponentOption). Useslots: { item: fn }. A leftovercomponent:row renders as a plain action row off itslabel.
Also removed: the DropdownExposed type — it described a close() template-ref member that Dropdown never implemented ([ADR-0012] keeps Dropdown's template-ref surface empty; v-model:open and the close slot prop cover it). Type-only, so the break is loud.
Dropdown — disabled state reaches the menu primitive
The trigger now forwards its disabled state (from button.disabled or a disabled fallthrough attribute) to the underlying menu primitive. Previously only the generated Button was natively disabled; a custom trigger slot with a disabled attribute could still open the menu via keyboard or synthetic clicks.
Select — #item-* slot prop renamed to item
#item-prefix, #item-label, and #item-suffix on Select expose item as the canonical scoped binding, matching Combobox and MultiSelect. The previous option key is removed with the rest of the deprecated surface (ADR-0008) — destructuring { option } yields undefined, silently. No runtime warning is possible (slot-prop destructuring isn't detectable), so grep for #item- slots destructuring option.
<!-- before -->
<Select :options="people">
<template #item-prefix="{ option }">
<Avatar :image="option.image" />
</template>
</Select>
<!-- after -->
<Select :options="people">
<template #item-prefix="{ item }">
<Avatar :image="item.image" />
</template>
</Select>Combobox — trigger sizing matches Select
Root renders as a transparent layout box so the trigger sizes like Select in flex/grid containers. Query decoupled from model in button mode.
Combobox / MultiSelect — #suffix slot replaces the chevron
New #suffix slot on Combobox (input and button modes) and MultiSelect, mirroring the existing slot on Select. Providing the slot replaces the default chevron — render an explicit chevron fallback when your content is conditional. Canonical use is an inline clear button. See Combobox/stories/Clearable.vue.
Combobox — condition authoritative for type: 'custom' rows
A custom row's condition({ query }) is now consulted even before the user types since opening, so it can fully gate its own visibility based on selection state and the typed query. Selectable rows are unchanged. This makes "create new" patterns expressible directly via condition, with no need for a dedicated createOption prop. See Combobox/stories/CreateNew.vue.
MultiSelect — #summary suppresses the phantom sizer
The trigger's default behavior pins a minimum width derived from the worst-case default summary (placeholder vs "N selected") so the trigger doesn't jitter as the count changes. That sizer can't predict custom text, so it's now skipped when #summary is provided — the trigger becomes content-sized and the consumer owns the width.
InputLabel — slot polish
The default required indicator is not rendered when #label is used (slot receives { required }). The labeling wrapper is dropped entirely when there is nothing to label.
Popover — v0 API removed (breaking)
Every member marked @deprecated is deleted, per ADR-0008. Nothing is aliased and nothing warns.
- Breaking, silent: the
#target,#bodyand#body-mainslots are gone. Vue drops an unknown slot without an error, so a missed call site renders a popover with no trigger, or an empty one. Use#triggerand#default. - Breaking, silent:
#triggerwires the click itself through reka'sPopoverTrigger. A click handler carried over from#targettoggles the popover a second time, so it opens and shuts on one click. - Breaking, silent: the
togglePopoverandupdatePositionslot props are gone.togglereplaces the first; reka repositions on its own, so the second has no replacement. - Breaking, silent:
placement,show,hideOnBlur,matchTargetWidth,trigger,hoverDelay,leaveDelay,popoverClassandtransitionare removed, and theupdate:showemit no longer fires. An unknown prop is ignored, so the popover renders in its default position and state. - Breaking, silent: attributes on
<Popover>are no longer inherited. They used to land on a wrapper the legacy#targetrendered;#triggeris as-child and renders no wrapper. Moveclassandstyleonto the element inside#trigger. - Breaking: the
PopoverPlacementandPopoverLegacySlotPropstypes are removed. UsePopoverSide+PopoverAlignandPopoverSlotProps. The import fails, so the build names every call site. - Fixed:
CalendarWeekDayEventpassedplacement="center"in month view, which is not a side and reached reka as one. It isside="bottom"+align="center"now. - Breaking, silent: the slot props are
{ open, close, toggle }.openis now the boolean state, matchingDropdown,Select,MultiSelect,HoverCardandSidebar; theopen()method it used to be had no callers, since#triggeropens itself.isOpenis gone — readopeninstead. A destructuredisOpenbecomesundefinedwith no error, so styling that depends on it stops applying silently. - Fixed:
MonthPickerstyled its panel throughpopoverClass, which had already become a no-op, so the panel rendered with no surface at all. It uses the standard panel shell now. - Fixed:
:dismissible="false"still closed onEscape. Only the outside-click channel was wired, whileCONTEXT.mddefinesdismissibleas covering both.
Before/after for each silent break is in the migration guide.
NestedPopover — removed (breaking)
- Breaking:
NestedPopoveris deleted. UsePopover. It never nested anything, and it was the library's last@headlessui/vue+@popperjs/corepopover —@popperjs/coreleavesdependencieswith it. The import fails, so the build names every call site.
Tooltip — vocabulary aligned with Popover and HoverCard (breaking)
- Breaking, silent:
placementis renamed toside, matchingPopoverandHoverCard. An unknown prop is ignored, so the tooltip keeps working and points at its default side. - Breaking, silent:
arrowClassis removed (P10 — no class-injection props). Style the arrow through[data-slot="arrow"]. It was documented as the arrow's fill but was mostly used to nudge the bubble's position, which the newoffsetprop does directly. - Added:
offsetsets the gap in px between trigger and bubble, matchingPopoverandHoverCard. The bubble is no longer pinned at 4px. - Added:
[data-slot="content"],[data-slot="bubble"]and[data-slot="arrow"]styling hooks. - Breaking, silent: the
#bodyslot is replaced by#content, which renders inside the bubble instead of replacing it.#bodyis not in P6's slot vocabulary, and it was the wrong shape: it stripped the bubble's surface, so six of the seven call sites in the apps hand-copiedrounded bg-surface-gray-10 px-2 py-1 text-xs text-ink-base shadow-xlto put it back. Moving to#contentusually means deleting that wrapper. Vue drops an unknown slot without an error, so a missed call site shows an empty tooltip. - Added:
barerenders#contentwithout the bubble shell, for content that brings its own surface — an image preview, say. The arrow still renders. This is the honest form of what#bodywas reached for. TooltipBubbleis no longer exported. It is the internal bubble shared byTooltipandButton, with no call sites outside the library.Tooltipkeeps#defaultas the trigger, deliberately. It is the one inversion in the library, recorded under P6: over 200 call sites use the<Tooltip text="…"><Button /></Tooltip>shorthand, and renaming the slot would move every one of them for no behavioral gain.
Before/after is in the migration guide.
HoverCard — open() and close() on the template ref
- Added:
open()andclose()on the component instance, matchingPopover. - The trigger slot's props are now typed (
HoverCardSlotProps) instead ofany.
BottomSheet — focus stays inside an open sheet
The sheet opts out of autofocusing its first field, so it does not pop the keyboard on a phone. That also left focus on the trigger behind the overlay, with nothing holding it — Tab walked the page behind an open modal. The sheet now takes focus itself on open. The keyboard still stays down.
Divider — action.handler removed (breaking)
Per ADR-0008, action.handler is deleted, not carried forward as a warning. Use action.onClick. Zero call sites across the census. Silent break — a leftover handler is dropped as an unknown key, so the action button renders but does nothing on click. Action mode preserves separator semantics for assistive technologies.
PageHeaderBackButton — to is now a fallback (breaking)
to used to be the destination. Setting it made every tap push that route. It is now used only when there is no in-app history to go back to, such as a cold load onto a deep link. Every other tap goes back through history.
A back button that always lands on one fixed route is not a back button. It drops the user wherever the page author guessed they came from, which is wrong for every other way into the page.
Migration: nothing to do if to already named the page users came from. It now applies only on a cold load. If you need an unconditional push, use a plain Button with your own router.push.
Editor — media captions moved off alt (breaking)
Text in an image's or video's alt no longer renders as a caption. Captions live in a separate caption attribute, serialised as data-caption. alt goes back to being the screen reader description only.
Existing alt values still parse and still round-trip untouched. They just do not display as a caption any more.
The editor no longer edits alt at all. The caption field used to write it; it now writes caption, and no other control took over alt. Set it from your own content pipeline until an alt field lands.
There is deliberately no fallback from caption to alt. Stored alt values are mostly upload filenames and emoji shortcodes, and a real caption cannot be told apart from those automatically. Showing all of them is worse than showing none.
Migration: to keep a caption visible, copy the text into caption. A one-off content migration can do that where you know the old alt values were captions.
HTTP transport — four paths collapse to one (breaking)
frappeRequest is the single transport. Removed from the root export:
request— the barefetchwrapper underfrappeRequest, now internal. UsefrappeRequest.createCall— no consumers in any app.initSocket— no consumers in any app; every one defines its own.socket.io-clientremains a dependency (resources/realtime.tsexports functions typed against itsSocket).
All three are build failures at the import.
call now honours setConfig (silent). It kept its (method, args, options) signature, the value it resolves to, and the { response, status, error } shape it hands onError, but it delegates to frappeRequest instead of building its own fetch. It had never imported getConfig, so requestBaseUrl and requestHeaders were ignored on every call() while frappeRequest respected them. Two consequences in apps that set either: call now goes to the configured base URL with credentials: 'include', and _server_messages from a call now reach serverMessagesHandler.
FrappeRequestError is now exported. frappeRequest threw it but nothing exported it, so a consumer could not type a catch.
frappeRequest — onError fired twice per failure (fix)
request() attached transformError with a trailing .catch, which also caught what transformResponse threw. Every failed HTTP response therefore ran onError twice. It now runs once, because frappeRequest marks an error it has already reported rather than because the rejection handler sees less. That distinction matters: an ok response whose body will not parse — Frappe answers an expired session with 200 and its login page, so response.json() throws — is a failure only that handler sees, and it still reaches onError.
A method name starting with http skipped the /api/method/ prefix (fix). The absolute-URL check was url.startsWith('http'), which matches the four letters rather than a scheme, so http_utils.api.run was fetched as a relative path. It now matches https?://.
frappeRequest also gained an explicit return type and passes its type argument through, so frappeRequest<Foo>() resolves to Foo rather than unknown.
login returned only message when requestBaseUrl was set (fix).login is the one endpoint that resolves to the whole body, so a caller can read full_name and home_page. The check compared the whole URL against /api/method/login, and requestBaseUrl makes that URL absolute, so it stopped matching and login quietly resolved to data.message. It now matches on the path.
FrappeUI plugin — one option left (breaking)
app.use(FrappeUI) accepts resources and nothing else, and no longer installs it by default.
socketioremoved. It defaulted totrue, so apps that also built their own socket opened two live socket.io connections per page load.callremoved. It installed a$callglobal with no consumers.configremoved (silent).setConfigis the entry point. One app passed it.resourcesno longer defaults totrue. The v1 resources Options API mixin —this.$resources,$getResource,$getDoc,$getListResource,$refetchResource— installs only onapp.use(FrappeUI, { resources: true }). Composition API resources are unaffected.resourcesPluginstays exported for direct installation.
Because a removed option is ignored rather than rejected, the plugin logs a dev-mode warning naming any option it does not accept and what to use instead.
Removed features fail loudly, in production too. A dropped option that evaporates is an annoyance; a dropped feature that evaporates is a mystery crash somewhere else. So:
- A component declaring a
resourcesoption withoutapp.use(FrappeUI, { resources: true })throws on creation, naming itself and the fix. - Reading
this.$resourceswith the option off throws the same advice. Both guards exist because Vue routes what a lifecycle hook throws through its own error handling, which only logs in production — a read throws straight into app code in every build. - Reading
this.$socketorthis.$call, the two globals the plugin stopped installing, throws a message naming the replacement instead of returningundefined. Assigning your own —app.config.globalProperties.$socket = io(…)— replaces the guard, before or afterapp.use(FrappeUI).
realtime: true on a v1 resource still degrades quietly to a non-realtime resource when no socket is set. That has always been its behaviour and this does not change it.
Data fetching (v2) — one request per submit
useDoctype's insert, delete, setValue, runDocMethod and runMethod, and useList's insert, setValue and delete, each held a single shared request. Two submits at once aborted one another, and every submit resolved from the same data, so a caller could receive another caller's answer or null. Each submit now sends its own request and resolves with its own response.
- Breaking: these eight members no longer carry the
useCallsurface. Removed:params,promise,url,reset,abort,execute,fetch,reload,isFetching,isFinished,canAbort,aborted. Each of them described one shared request, which no longer exists. - What is left, on all eight:
submit(),data,error,loadingandisLoading().loadingis true while any submit is in flight. - New:
delete.isLoading(name)andsetValue.isLoading(name)on bothuseDoctypeanduseList. This replaces thedelete.loading && delete.params.name === row.nameidiom, which showed the same spinner on every row once two deletes overlapped. - New:
insert.isLoading()on both, taking no argument. A new document has no name to key on, so it answers for the whole method — the same value asinsert.loading. It exists so all eight methods read the same way. runDocMethod.isLoading(name, method)andrunMethod.isLoading(method)keep their signatures and now answer correctly with several submits in flight. They previously compared the shared URL, so only the newest submit read as loading.dataanderrorbelong to the submit that started last, not the one that answered last. A slow submit that comes back after a newer one is dropped: it writes nodata, writes noerrorand clears nothing. It still answers its own caller with its own outcome — resolving with its response, or rejecting with its error.- The winning submit writes
dataanderrortogether. Success setsdataand clearserror. Failure setserrorand leavesdataalone. datais no longer reset on failure. The old shareduseCallsetdataback tonullwhenever a response came back not-ok. It now keeps the last successful response. Readerror, notdata, to tell a failed submit from a successful one.erroris no longer cleared when a submit starts. Clearing it there erased the error of a sibling submit that was still in flight. An error stands until the newest submit settles.submit()now rejects on any failure. It resolves with the response or rejects with the error — one channel, not two. A failedvalidatealready rejected; a failed request used to resolve withnull. Both reject now, and a server that answers withnullresolves withnull.useList'sinsertanddeletenow send tobaseUrl, which they silently dropped.setValuealready used it, so all three write methods now agree.useDoctypewas never affected — every one of its methods already passedbaseUrl.
Data fetching (v2) — useFrappeFetch off the root export (breaking)
useFrappeFetch removed. It is the raw createFetch instance useCall, useDoc and useList are built on — headers, response parsing and error shaping, and nothing above that: no URL building, no params, no caching, no typed return. No app imports it. It is a build failure at the import; use useCall for a whitelisted method, useDoc for one document, useList for a query.
FrappeResponseError is now exported. The composables raise it on a Frappe error response and put it on .error, and submit() rejects with it, but nothing exported the class, so a consumer could not narrow the error. Same gap FrappeRequestError closed for frappeRequest.
Data fetching (v2) — docs, and the sidebar splits from Resources
useCall, useDoc, useList, useDoctype and useNewDoc each get a docs page for the first time, under a new Data Fetching sidebar section — useCall for a whitelisted method, useDoc for one document, useList for a query, useDoctype for write-only access to a DocType, useNewDoc for a draft-and-insert form.
The old Data Fetching section is renamed Resources and keeps its three pages (Resource, List Resource, Document Resource) unchanged. Both sections link to each other: Resources stays fully supported through 1.x; the new composables are the recommended layer for new code.
Data fetching (v2) — useNewDoc lost reactivity after submit (fix)
useNewDoc built its return value with reactive({ ...out, submit, doc }). Spreading a reactive() proxy reads every ref and computed once and freezes the result, so data, error and loading stopped updating the moment the object was built — a template bound to newDoc.loading never saw it flip. The return value is now built by mutating the underlying object in place, so its properties stay live.
Data fetching (v2) — initialData did nothing on useCall and useList (fix)
Both documented an initialData option to show a placeholder before the first response. Neither worked. useCall passed it straight to the underlying fetch, which expects the wrapped { data: ... } shape the API actually returns — the unwrapped value was invisible, so call.data stayed null until the first response. useList's data is read from a separate list built in afterFetch, which initialData never touched, so list.data stayed null the same way. Both now show the seeded value immediately, as documented.
Root composables and directives — renamed and shrunk
Every change below is a loud break: the import line fails, so the build, the type-check or the dev server says so. No silent behavior changes, and nothing here needs a migration-guide before/after.
useTheme is now useColorScheme. theme means color tone everywhere else in the library (theme="blue" on a Button, ~300 sites), so the light/dark composable stops competing for the word.
// before
import { useTheme, type Theme } from 'frappe-ui'
const { currentTheme, setTheme, toggleTheme } = useTheme()
// after
import { useColorScheme, type ColorScheme } from 'frappe-ui'
const { colorScheme, setColorScheme, toggleColorScheme } = useColorScheme()colorSchemeis read-only. Assigning to the oldcurrentThemeref moved the ref without settingdata-themeorlocalStorage, so the app silently desynced. Go throughsetColorScheme.initializeThemeandgetSystemThemeare gone.useColorScheme()already restores the saved preference and follows the OS on its first call.- The
data-themeattribute and thethemelocalStoragekey are unchanged. App CSS targeting[data-theme='dark']and users' saved preferences keep working.
Nine scroll members become two.
// before
import { activeScrollContainer, useScrollContainer, scrollToTop } from 'frappe-ui'
const { isScrolled } = useScrollContainer({ threshold: 12 })
// after
import { shellScrollContainer, useShellScrolled } from 'frappe-ui'
const scrolled = useShellScrolled({ threshold: 12 })
shellScrollContainer.value?.scrollTo({ top: 0, behavior: 'smooth' })| Removed | Use instead |
|---|---|
activeScrollContainer | shellScrollContainer |
useScrollContainer().isScrolled | useShellScrolled() |
useScrollContainer().el | shellScrollContainer |
getScrollContainer() | shellScrollContainer.value (works outside setup() too) |
scrollTo(o) | shellScrollContainer.value?.scrollTo(o) |
scrollToTop() | shellScrollContainer.value?.scrollTo({ top: 0, behavior: 'smooth' }) |
registerScrollContainer / unregisterScrollContainer | internal to DesktopShell / MobileShell |
UseScrollContainer, UseScrollContainerOptions | no replacement needed |
The shell prefix is deliberate: both resolve only while a DesktopShell or MobileShell is mounted. useShellScrolled now warns once in development when no shell is registered, instead of silently reporting false forever.
Directives are vFocus and vOnOutsideClick. <script setup> auto-registers a directive only when the binding is named vFoo, so the old names forced a manual alias at every call site.
<!-- before -->
<script setup>
import { onOutsideClickDirective as vOnOutsideClick } from 'frappe-ui'
</script>
<!-- after -->
<script setup>
import { vOnOutsideClick } from 'frappe-ui'
</script>visibilityDirective is removed with no replacement (0 call sites). Use an IntersectionObserver directly, or @vueuse/core's useIntersectionObserver.
useScreenSize, useIsMobile and ScreenSize are no longer exported. They were a thin wrapper over a resize listener that the library never used itself. Copy the ~20 lines into your app, or use @vueuse/core's useWindowSize / useMediaQuery.
code-editor subpath — folded into experimental (breaking)
Breaking:
frappe-ui/code-editoris removed.CodeEditor,CodePreview, andloadLanguagemove tofrappe-ui/experimental(ADR-0010). One downstream file imported the old subpath; the fix is a one-line import change.ts// before import { CodeEditor, CodePreview } from 'frappe-ui/code-editor' // after import { CodeEditor, CodePreview } from 'frappe-ui/experimental'
experimental barrel — tidy and FrappeUIError
- Breaking:
LabelingWrapperis dropped fromfrappe-ui/experimental. It stays exported from its own barrel (src/components/InputLabeling) —Combobox,Select,MultiSelect, andMultiEmailInputimport it from there internally — only theexperimentalre-export had zero external importers, so only that goes. The only member cut in the barrel tidy. FrappeUIErroris now exported fromfrappe-ui/experimentalas a type. A consumer previously hand-declared a structural copy of it because it wasn't re-exported — that copy can now be dropped in favor of the real type.
tsconfig.base.json — cleaned up (breaking for extenders)
- Breaking:
tsconfig.base.jsonno longer setstypes(vitest/globals,unplugin-icons/types/vue,node). If your app extends this file and relies on any of these globals, addtypesto your owntsconfig.json. Without it,tscfails with a missing-global error (e.g.Cannot find name 'vi') the first time a global that used to come fromvitest/globalsorunplugin-icons/types/vueis referenced.noEmitstays — it's needed to keepallowImportingTsExtensionslegal — but thedeclaration/emitDeclarationOnlypair (contradictory alongsidenoEmit, and unused by frappe-ui's own build) is gone.
./hljs-theme.css export removed
- Breaking:
frappe-ui/hljs-theme.cssis no longer exported. It had zero importers. The underlying file (experimental/TextEditor/hljs-github.css) ships until the deprecatedTextEditoris removed.
pageMetaPlugin — removed
- Silent break:
pageMetaPluginand the global mixin it installed are gone. A leftoverpageMeta()component option still compiles but is never read, sodocument.titleand the favicon quietly stop updating. See the migration guide. usePageMetais unchanged and now exports itsPageMetatype.
GridLayout — removed (breaking)
- Breaking:
GridLayoutis no longer exported. It was a thin passthrough togrid-layout-pluswith no docs page and no tests. The import fails, so the build names every call site. Depend ongrid-layout-plusdirectly. grid-layout-plusis dropped fromdependencies— it had no other importer left insrc/.- Two bugs in the deleted component, so consumers wiring up
grid-layout-plusthemselves should expect different behavior:colsandrowHeightwere read once at setup inside areactive()options object, notcomputed, so changing either prop after mount did nothing.- the drag placeholder color was a hardcoded
#b1b1b1, not a theme token, so it ignored dark mode.
App shell family — brought to bar
DesktopShell, MobileShell, MobileNav, Rail, PageHeader, ScrollArea, and FrappeUIProvider all keep their current exports and names.
- Every slot across the family now has a documented description, and each component has a docs page, a story, and cypress tests (several had none).
- Breaking, silent:
PageHeaderMobile's#left/#rightslots andPageHeaderMobileTitle's#iconslot are renamed to the shared#prefix/#suffixvocabulary (PHILOSOPHY.md P6 forbids type-specific slots like#iconoutsideButton, and#left/#rightwere never in the vocabulary). Vue drops content passed to an unknown slot name with no error, so the old names don't warn — they just stop rendering. See the migration guide. ScrollAreagets atypes.ts(ScrollAreaProps,ScrollBarProps,ScrollAreaExposed) anddata-slot="scroll-area"/"scroll-area-viewport"/"scroll-area-scrollbar"/"scroll-area-thumb"styling hooks — it had none.viewportElementon the template ref is now typed viaScrollAreaExposed. (SettingsDialog'sSettingsBodyexposes the same shape today but isn't wired to this type yet — that's tracked under SettingsDialog's own sweep.)FrappeUIProvider's source directory moved fromsrc/components/Providertosrc/components/FrappeUIProviderto match its file name. Purely internal —import { FrappeUIProvider } from 'frappe-ui'is unaffected.- Breaking:
FrappeUIProviderPropsis no longer exported. The component has no props, so the type was empty and never wired todefineProps— freezing it now would lock in nothing. Zero known consumers. The mismatched directory name had made the whole component invisible to the docs generator, so it previously had no docs page.
frappe and drive subpaths — removed (breaking)
- Breaking:
frappe-ui/frappeis removed and thefrappe/directory is deleted (rule 6: frappe-ui is a dumb library; decided in #867, moved in frappe/frappe#41671).useTelemetry,telemetryPlugin,useOnboarding,GettingStartedBanner,IntermediateStepModal,HelpModal,showHelpModal,minimize,TrialBanner,SignupBanner,DataImport,Link,FilterandLinkPropsnow live in@framework/ui. TheLinkandFilterthere are supersets (Link:redirectable/editableprops,redirect/editemits;Filter:useFilters,parseFilters/serializeFilters, operator registry). - Breaking:
OnboardingSteps,HelpCenterandshowHelpCenterare removed with no standalone replacement — zero call sites across all consumer apps (they still powerHelpModalinside@framework/ui). - Breaking:
frappe-ui/driveandfrappe-ui/drive/*are removed with no replacement. No app imported them — the drive app owns the live copy of all six components. - The
contentexport fromfrappe-ui/tailwindand the docs no longer list afrappe/**glob; apps hand-maintainingnode_modules/frappe-ui/frappe/**intailwind.config.jsshould drop the line.
Deprecation log
| API | Replacement | Notes |
|---|---|---|
Divider.action.handler | Divider.action.onClick | Removed — silent; key dropped, click does nothing |
Password.value prop | v-model / modelValue | Removed in 1.0.0 (ADR-0008) |
Rating.rating_from prop | max | Removed — silent; prop ignored |
Rating.readonly prop | disabled | Removed — silent; prop ignored |
Switch.change emit | update:modelValue / v-model | Removed — silent; listener never fires |
Switch.labelClasses prop | data-* styling hooks | Removed — silent; prop ignored |
Checkbox.padding prop | padded / data-* styling hooks | Removed — silent; prop ignored |
Dropdown { group, items } | { group, options } | Removed — silent; renders empty, dev-only warning |
Dropdown.placement prop | align | Removed — silent; falls back to align="start" |
Dropdown/ContextMenu component: rows | slots: { item: fn } | Removed — silent; renders label-only row, dev-only warning |
DropdownExposed type | v-model:open / close slot prop | Removed — loud; described an expose that never existed |
Select #item-* slot prop option | item | Removed — silent; { option } destructures to undefined |
Input.vue | TextInput | Removed in 1.0.0 (ADR-0008) |
Autocomplete | Combobox or MultiSelect | Removed — import fails |
GridLayout | depend on grid-layout-plus directly | Removed — loud; import fails |
FormControl type='autocomplete' | type="combobox", or Combobox standalone | Removed — silent; dev-only console.error |
DatePicker family placement | side + align + offset | Removed — silent; inert extra attribute |
DatePicker family autoClose | keepOpen (inverse) | Removed — silent; inert extra attribute |
DatePicker family allowCustom | typeable: false | Removed — silent; inert extra attribute |
DatePicker family readonly | typeable: false | Removed — silent; inert extra attribute |
DatePicker family inputClass | class on the component element | Removed — silent; inert extra attribute |
DatePicker family value prop | v-model / modelValue | Removed — silent; inert extra attribute |
DatePicker family #target slot | #trigger | Removed — silent; slot content stops rendering |
TimePicker.scrollMode | none (always centered) | Removed — silent; inert extra attribute |
DateTimePicker.minDateTime | min | Removed — silent; constraint no longer enforced |
DateTimePicker.maxDateTime | max | Removed — silent; constraint no longer enforced |
TimePicker.minTime | min | Removed — silent; constraint no longer enforced |
TimePicker.maxTime | max | Removed — silent; constraint no longer enforced |
TimePicker.selectAll() / .blurInput() | none — dead, no callers | Removed — loud; template-ref member gone |
useDatePicker composable | use picker components directly | Removed — loud; import fails |
getDate / getDatesAfter / etc. | use picker components directly | Removed — loud; import fails |
MonthPicker | Select | Removed — loud; import fails |
FeatherIcon | lucide-* strings (or a Component) | Removed — import fails; feather-name props render nothing, dev-warns once |
Card | layout markup | Removed in 1.0.0 (ADR-0008), import fails |
ListItem | layout markup | Removed in 1.0.0 (ADR-0008), import fails |
Toast (SFC) | imperative toast(...) API | Removed in 1.0.0 (ADR-0008), import fails |
Dialog legacy options blob | flat top-level props | Removed — silent; inert attr |
Dialog disableOutsideClickToClose | dismissible (inverted) | Removed — silent; inert attr |
Dialog #body* slots | #default / #title / #actions | Removed — silent; renders nothing |
Dialog icon.appearance | icon.theme | Removed — silent; icon loses tone |
Dialog action onClick callable context | { close } object | Removed — throws on call |
Dialog template-ref close() | v-model:open / close slot prop | Removed — throws on call |
ConfirmDialog component | dialog.confirm() / dialog.danger() | Removed — import fails |
confirmDialog() | dialog.confirm() | Removed — import fails |
FileUploader.uploadArgs | flat props (private, folder, doctype, docname, fieldname, uploadEndpoint, optimize) | Removed — silent; inert attr |
FileUploader template-ref inputRef | openFileSelector slot prop | Removed — throws on call |
FileUploader slot prop error | always string | null, was unknown | Changed — silent; .message access renders nothing |
useFileUpload / FileUploadHandler unset privacy | explicit private / is_private | Default changed — silent; now resolves to private |
fileToBase64, formatBytes, getMaxFileSize, fileSizeLimitMessage | none (internal only) | Removed — import fails |
frappe-ui/charts ColorScheme type | root ResolvedColorScheme (re-exported from frappe-ui/charts) | Removed — loud; type import fails |