v5 #6610
Replies: 41 comments 12 replies
This comment was marked as off-topic.
This comment was marked as off-topic.
This comment was marked as off-topic.
This comment was marked as off-topic.
|
I am not sure how much work this needs but supporting UnoCSS (with the tailwind preset) would be fantastic. Our team is used to it (or rather some of its other presets) instead of tailwind and new projects are currently split between Nuxt UI and another component library for that reason |
|
It would be great if the As for 4. (splitting components) I hope you won't go the way of shadcn/ui where you need to include 5 components just to show a nav menu with some links. I like the approach of using props and objects for that more. It's also more typesafe, since afaik there's no way of restricting the type of a child element/component. |
|
@septatrix you can try my preset that enables Nuxt UI to work with Unocss https://github.com/lehuuphuc/unocss-preset-nuxt-ui |
|
@Blechlawine We won't go the shadcn way of course not, what I meant by splitting components is in some specific cases it would be easier to override if moved to its own component like for example the Header mobile menu (https://github.com/nuxt/ui/blob/v4/src/runtime/components/Header.vue#L178). |
|
@lehuuphuc You can make a PR to add your preset to the Community page if it's working 😊 https://ui.nuxt.com/community |
I don't want to derailt this conversation any further but just a quick question, I assume this will not stop tailwind to run and compile the styles in the background? |
This depends on your project setup. My library is a preset for the unocss config, used alongside the "wind4" preset. It adds the necessary classes for Nuxt UI to function without tailwindcss. |
|
Hi @benjamincanac I think you should also add rethinking components for layout as we discussed here: #4955 Right now |
|
Not sure if this is possible, but it would be nice to have the possibility to add new tailwind classes on the fly during execution. Currently classes not used in the code are not complied so I cannot set such a class unless it is safelisted via |
|
I see two nice to have features for this library:
|
|
Since v3, |
|
How about standalone components, like Shadcn? Why? - Certain components can be customized much more deeply depending on specific needs, customizations that aren’t possible using only props or configuration options. For example, the ColorPicker could have a horizontal hue slider instead of a vertical one, and it could also include an alpha slider. These kinds of changes aren’t possible unless you modify the component at the source level. Yes, the components can already be extracted and used as standalone components with some modifications, by removing Nuxt-specific dependencies tied to the node_modules source level, but... just an idea. |
|
I think it would be a great addition to generalize the Chat components to make them fully agnostic. Currently, I've managed to build chat UIs by using the However, for v5, we should take it a step further and consider the following improvements:
This approach keeps the core component ultra-flexible and clean, while still providing an easy bridge for developers working with popular AI SDKs. What do you think? |
|
@maximepvrt Recently I experimented with Chat components in some project and thinking about somewhat similar. It would be great if Nuxt UI will provide the generic Chat as basic set: input + message + messages components, + easy way to enhance it by adding some custom things for messages (attachment previews, charts, whatever else can be imagined, ...) - something like current AI Chat can support via custom tools. And that generic Chat components should be easily turned into AI chat by adding current Chat components - reasoning, shimmer, tool, prompt, submit. |
|
Nuxt UI is a great lib, and I’ve been using it a lot lately. For example, instead of export interface NavigationMenuItem extends Omit<LinkProps, 'type' | 'raw' | 'custom'> {
// ...
[key: string]: any;
}it could be like this, so TS knows the shape of custom fields: export type NavigationMenuItem<
TMeta extends Record<string, unknown> = {}
> =
Omit<LinkProps, "type" | "raw" | "custom"> &
TMeta & {
label?: string;
icon?: IconProps["name"];
// ...
[key: string]: unknown;
}Or maybe even cleanest way - to avoid index signature and collision with interface props, like this: export interface NavigationMenuItem<TMeta = unknown> {
label?: string;
icon?: IconProps["name"];
// ...
meta?: TMeta;
}Also it might be good idea to get rid of |
|
For the css variables it would also be great if the names were more clear if they're lighter/darker than the background. With the current names |
|
A couple of things! De-Dashboarding / thin wrappersA recent discussion with @benjamincanac in #6340 about removing the Dashboard class of components. From what I can see Resize / split-panesA resize composable would be quite cool for generalised usage:
I'm a big fan of Blender-style UIs that let you split and customise the layout tree with drag-drop/split. That'd be the fun moonshot on top for the ultimate dashboard layout system.
Mobile interactionsThere are a couple of weird mobile interactions that make it hard to build a proper web app. In particular I make use of the Sizing consistency#6581, another user mentions here that maybe badges and buttons should have aligned and additional sizes, could be a good candidate for v5 with breaking changes. Vertical orientationI'd also love vertical button orientation (mentioned in #6527), and I recently added an update to the docs for vertical tab buttons. This could also have the Theming & customisation toolingI'd love more customisation and theming tooling, previewing the templates in different themes. I made a small tailwind-colors grayscale editor because I thought light mode was a bit too colour-less. This is maybe a bit too far, but I liked shadcn's recent create-page updates or Shadcn Studio's theme customiser and presets. |
Add support for MDC syntax to UEditor.It would be nice to get also composable to alter conversion from MDC to tipTap and TipTap to MDC. |
|
Since Nuxt UI is already planning a major primitives migration (e.g. Reka UI 3), I'm curious if Vuetify 0 was evaluated as well. It is also built around a headless/composable approach, while bringing years of maturity and experience from Vuetify with complex enterprise components. |
|
@alihardan I don't think we'll be switching our primitives, the Reka UI v3 migration will be pretty smooth with some internals optimizations which should not bring too many breaking changes. Switching to something like https://0.vuetifyjs.com/ or https://ark-ui.com/ would break everything and make the transition hell like between v2 (which was based on Headless UI) and v3. |
|
I am adding my view regarding this issue/ discussion about Epic C: #6716 (comment) @benjamincanac I think instead of |
|
Mmmh the issue is https://www.tailwind-variants.org/docs/composing-components#using-the-result |
|
While experimenting with theme editor, I notice one really great slider to have would be a set of abstracted spacing and padding tokens to control how compact or spaced out everything feels. You can adjust --spacing but this is too strong a lever over too many other things, you can try it here in the 'general' menu. Shadcn abstracts it away from the user further with 'styles' in their editor https://ui.shadcn.com/create but uses underlying |
|
I would like a better time picker, the current one is very aligned to 12h formats making it hard to use in other locales (or even non-24h international english) |
|
Other component collapse opportunities: BlogPost, PageCard and Card could all be one component. Perhaps Card should stay separate that the other 2 are based off. |








Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Nuxt UI v5
No firm date yet, this tracks the direction for the next major and I'll keep it updated as things land.
v5 is a theming-led major. The flagship is Design Tokens v2, a redesign of our CSS variable system. It genuinely needs breaking changes (token restructure, default appearance and contrast shifts, scoped re-resolution), so the API renames and restructures we've long parked ride along and you migrate once, with a codemod.
The other pillar is rebasing on Reka UI v3 (unovue/reka-ui#2721), the headless foundation we're built on. It brings
useRender(which drops the internalPrimitive/asChildwrapper), per-component imports, an ESM-only build, a performance overhaul and full Shadow DOM support. Reka keeps its public component API intact, so it's mostly an internal shift for us, but it underpins the whole release and brings the new Drawer primitive that lets us dropvaul-vue.A few things stay the same on purpose. We're not going the shadcn copy-paste route, components stay upgradeable through npm. Semantic color names (
primary,secondary,success,info,warning,error,neutral) and:uislot names remain versioned public API. Numbered shade scales (--ui-color-{c}-{shade},bg-primary-600) stay too, and v5 adds role tokens on top rather than dropping them.Phase 0: non-breaking precursors (ship in v4 now, de-risk v5)
Everything here is additive and unblocks/derisks v5. Independent of the v5 cut.
:uiper-slot replace — shipped v4.9.0 (feat(theme): allow replacing slot classes with a function #6562) as a(defaults) => classesfunction form (closes 🎨 Allow replacing all UI classes with new set of classes? #4953/Cant override styles #6169). Default stays merge (replace-by-default rejected as a footgun); only a docs-wording fix (Unable to remove classes from UI elements with ui-prop overwrites #5803) remains.unstyledmode — shipped v4.9.0 (theme.unstyled, feat(module): addtheme.unstyledoption #6551).componentDetectionin the Vue package — shipped in v4 (feat(vue): supportexperimental.componentDetection#6731, merged alongside perf(vue): skip rewriting unchanged templates #6730; relates Everything gets loaded on a fresh app #6296). Ports the theme-CSS tree-shaking (experimental.componentDetection) to the Vite/Vue unplugin build; it was Nuxt-only, so the Vue build always emitted@source "./ui"and every app shipped CSS for all ~168 theme files. Mechanism differs from Nuxt by necessity: Tailwind widens a file@sourceinside the Vite root to a whole-directory scan (upstream bug, fix submitted as Don't scan sibling files of a concrete@sourceinside an auto source root tailwindlabs/tailwindcss#20406), so narrowing the@sourcelist is a no-op for the Vue layout (node_modules/.nuxt-ui). Instead the plugin runs detection in itsconfighook and writes undetected components' theme files blanked via thetheme.unstyledmachinery (applyUnstyled) — works on every Tailwind 4.x,ui.cssbyte-identical either way,#build/ui/*aliases andcomponents.d.tstypes untouched. starter-vue: 187.53 → 84.15 kB (24.72 → 12.25 kB gzip, after theapplyUnstyledbase-shape fix). Additive (opt-in, off by default; Nuxt output byte-identical, zero snapshot changes). De-risks the Epic E flip to on-by-default across Nuxt + Vue; live HMR re-detection deferred to a restart-only note (→ Epic E follow-up).Epic A: Design Tokens v2 (flagship · breaking)
Full token system, freed from additive-only. Three tiers (primitive → theme → calculated),
-foregroundpairing convention, derive-don't-scatter (color-mixrecipes generated once on the accent scope classes, not/10//75literals across 168 theme files).A1: Per-color roles through one accent indirection (fixes #1284, #6588, #3307 · subsumes most of A5/#5657)
Revised 2026-08-20; mechanics verified against a real Tailwind 4.3.3 build + Chrome computed styles. The previous sketch kept classes keyed on the color name, so every theme still multiplied 6 variants × 7 colors. The fix is one indirection: themes consume a single
--ui-accent-*role set, and thecolorprop resolves to a scope class that points--ui-accentat an alias. Radix Themes ships the same shape (per-componentdata-accent-colorre-pointing--accent-*vars); we keep our orthogonalcolor×variantAPI and shade scale on top.src/templates.ts(≈8 rules for the whole library, not per component)::root—var()inside a custom property substitutes where the property is specified, so a:root-level recipe computes against:root's accent and every scoped color inherits primary-tinted derived values (verified broken in Chrome).--ui-{c}aliases stay on:root/.darkincolors.tsas today; only the derived set moves onto the scope classes. Because recipe and base sit on the same element, every override path re-derives correctly: alias class, wrapper var override,[--ui-accent:x]utility, inline style (all four verified).src/templates.ts@theme inline:--color-accent(-foreground/-hover/-muted/-soft/-soft-hover/-ring)→var(--ui-accent-*)so themes writebg-accent,text-accent-foreground,hover:bg-accent-hover,ring-accent-ring. Verified: opacity modifiers (bg-accent/10) compile tocolor-mix(… var(--ui-accent) …)with the@supportsfallback — same output shape as today's/10classes, so no per-class size regression — and tailwind-merge groupsbg-accentas a bg-color exactly likebg-elevatedtoday (bg-accent+bg-red-500→bg-red-500,bg-accent/10+bg-accent-softmerge).variantonly — ~42 entries → 6 per component across the 49 themes with per-color compounds; thecolorvariant map becomes one shared constant (likefieldGroupVariant) applying the scope class. This is the biggest CSS deletion in v5 (~6/7 of every colored class set). Caveat: teleported slots (Select/SelectMenu content, etc.) don't inherit from the trigger, so the scope class is applied on teleported slot roots too (tv per-slot variant, mechanical).neutralinto the accent shape so it stops being a bespoke class set (bg-inverted/bg-elevated/ring-accented/text-default/text-muted, duplicated across ~30src/theme/*.ts)..accent-neutralpoints the accent tokens at neutral's role values (solid →--ui-bg-inverted,-soft→ elevated surface,-muted→--ui-text,-foreground→--ui-text-inverted,-ring→var(--ui-border-accented)solid vs the chromatic 50% mix — the derived set must cover the superset of chromatic + neutral differences) socolor="neutral"stays visually identical while sharing the same variant classes. This is what unlocks neutral-as-primary (feat(module): supportprimary: 'neutral'in app config #6608, closed):primary: 'neutral'just re-points.accent-primaryat neutral's values and full cross-variant parity falls out. Not doable in v4 — chromatic variants are opacity blends of one color while the neutral variant is distinct solid surface tokens, so a pure--ui-primaryswap only matchessolidand outline/soft/ghost drift (why feat(module): supportprimary: 'neutral'in app config #6608 was closed rather than shipping half-parity). The neutral tier utilities (text-default/muted/dimmed/toned/highlighted,bg-default/elevated/accented/inverted,border-*,ring-*,divide-*) stay public API — ~700 usages acrosssrc/themeare structural (Card/Modal surfaces, headings, descriptions, placeholders, separators), only the duplication inside the ~30 neutral compound sets is deleted.text-inverted(34×) /bg-inverted(50×) shrink toward value-level tokens referenced by the recipes (--ui-accent-foreground, neutral solid), with a handful of class usages left (Tooltip, Kbd).color="indigo"/color="#5865F2": inline style on the root sets--ui-accent, dark shade flip vialight-dark()(body already setsscheme-light dark:scheme-dark). Tailwind's theme-var pruning can't break it: inline the fallback fromtailwindcss/colorsthe waycolors.tsalready does (var(--color-indigo-500, oklch(58.5% 0.233 277.117))— verified identical to the 4.3.3 emitted palette). Aliases keep the class path (no inline styles → CSP-clean, zero SSR payload change); raw values are the documented escape hatch (needsstyle-src-attr 'unsafe-inline'under strict CSP) and docs keep steering to aliases —color="red"scattered in an app kills global retargeting.--ui-warning-foregroundto a dark value (yellow needs dark text) — now just the.accent-warningoverride. Open question 2: explicit token vs autooklch(from var(--ui-accent) …)luminance contrast.before:bg-elevated/50(≈1.03:1, also the kbd focus indicator) in the 11 themes that use it (listbox/select/input-menu/command-palette/dropdown-menu/context-menu/navigation-menu + editor/content/prose themes). Open question 3: accent fill (bg-accent+text-accent-foreground, native-select pattern) vs neutral + persistent non-color cue. (Neutral fill alone can't hit 3:1 / SC 1.4.11.)UButton,UBadge, … colors on colored backgrounds #3307 on-colored-surface — a solid colored surface (Banner/PageCTA) re-scopes--ui-text/--ui-text-mutedto the color's foreground for its subtree (Material "on-primary"); removes the need for areversecolor. Same mechanism as the scope classes (adata/class hook re-pointing text tokens).A2: Surface / Overlay / Elevation
--ui-bg*ramp into a re-scopable one keyed by[data-elevation="N"]. Level 0 == today (byte-identical). Generate scopes (don't hand-write) in the build-time token CSS (templates.ts—colors.tsgoes away, see A6); ladders are neutral-index offsets:bg white→50→100→200,muted 50→100→200→300,elevated 100→200→300→400,accented 200→300→400→500bg 900→800→700→600,muted 800→700→600→500,elevated 800→700→600→500,accented 700→600→500→400color-mixadditive variant kept as documented opt-in.--ui-bg-overlay(lightwhite+ real shadow; darkneutral-800, lighter than surface). Floating components (DropdownMenu/Popover/Tooltip/Modal/Slideover/ContextMenu/CommandPalette) setdata-elevation="1"/ consume it on content root.--ui-shadow-elevatedwith top inset hairline in dark; map--box-shadow-elevated→shadow-elevated. Also expose a general--ui-shadow-*scale bridged to--shadow-*(mirrors Tailwind's current defaults, retunable in one place) — what docs(theme): add theme studio #6675's shadow axis consumes instead of static class bundles.elevationprop on container components (Card etc.): atvvariant{0,1,2,3}writing:data-elevation. Likely a shared composable/mixin so every surface component gets it. Escape hatch:data-elevation="1"on any element.ssr()modifier chain fromsrc/theme/tabs.tsverbatim (in-[[data-slot=list]:not(:has([data-slot=indicator]))]:data-[state=active]:before:shadow-none); a barebefore:shadow-nonewon't tailwind-merge against it. Any consumer restyling Tabs hits this. The shadow should ride a token, or the modifier should be reachable.--ui-border-width/--ui-ring-offset(A4) should make width a token instead.A3: Motion tokens
--ui-ease-*curated curves — shipped early in v4 (PR fix(theme): unify motion easing and respectprefers-reduced-motion#6742):--ui-ease-out: cubic-bezier(.23,1,.32,1)(overlay + accordion/collapsible enter and exit) and--ui-ease-fluid: cubic-bezier(.32,.72,0,1)(Slideover slide), adopted in thetv()theme, fixing theease-in-on-exit bug across every overlay. Reduced motion handled by redefining the scale/slide presence keyframes to opacity-only underprefers-reduced-motion. Documented on the CSS Variables page. (Naming settled here:--ui-ease-fluid, not--ui-ease-drawer.)--ui-animate-*named animations (skeleton/caret-blink/spin-fast) + keyframes,--ui-skeleton-animationtoggle, tooltip delay tokens. Bridge to--ease-*/--animate-*.data-reduce-motionglobal — extend themotion-reduce:variant to honor bothprefers-reduced-motionanddata-reduce-motion="true"(natural<UApp :reduce-motion>prop). Supersedes coarsetheme.transitions. (v4 already reduces the presence keyframes underprefers-reduced-motion; this adds the explicit opt-in override on top.)A4: Structural tokens + primitives + vibrant
--ui-disabled-opacity,--ui-border-width,--ui-ring-offset,--ui-backdrop(tokenized),--ui-cursor-*,--ui-font-heading/--ui-font-body+ afontSizescale.--ui-prose-flow/--ui-prose-leading/--ui-prose-size+--ui-prose-scale-ratio, so a context retunes from one wrapper (compact chat vs docs) instead of an ~11-lineprose:uiblock (shadcn-typeset-inspired, on our component model — nonot-typesethatch). v5, not v4: it only pays off with a clean modular scale as the new default. Retrofitting onto today's hand-tuned values gives ugly coefficients (*2.4,*1.6,*0.3) and asizeknob that can only rescale, not reshape (couldn't flatten a chat's heading scale). Reshaping the default scale is a default-appearance shift → breaking. Consumes the--ui-font-heading/-bodytokens above; a density API (presets / a<Prose>primitive, vs adisplay:contentswrapper trick) rides Prose × Comark (Epic C).A5: Scoped re-resolution (fixes #5657)
--ui-accentat point of use, so overriding--ui-primaryon any wrapper re-resolves the whole subtree (verified in Chrome: wrapperstyle="--ui-primary: …"retints children's base and derived hover/soft; a nestedcolor="error"inside stays error). Three override levels to document, broadest to narrowest (narrower wins): alias var on a wrapper,colorprop, element-level[--ui-accent:x]/ inline style. Remaining actual work: the global tokens consumed outside accent scopes (--ui-bg,--ui-text, …) still need the scale→alias flatten incolors.ts/templates.ts, plus the docs FOUC scripts.A6: Theming surface
[data-theme]var blocks + theme generator + documented stable token contract +llms.txt. Switching is already doable by hand today at the alias layer (redeclare--ui-primary,--ui-bg, … under[data-theme='x']and toggle the attribute — worth documenting now); scale-var overrides need A5, and the v5 item is generating the blocks from config.colors.tsruntime plugin — aliases stay as names (component defaults, semantic branching like Form'scolor="error",bg-primary-600ramps, global retargeting) but the palette mapping is emitted inui.cssinstead of reactively generated +useHead-injected: kills the SPA hydration dance, the docs FOUC scripts (A5), and shippingtailwindcss/colorsto the client for SSR apps (the oklch fallback table stays only for the raw-palette escape hatch). Runtime switching (docs ThemePicker / Epic G playground) becomes writing ~a dozen vars on:rootor toggling[data-theme]— the flat-blocks direction above. Breaking: reactiveappConfig.ui.colorsis a documented workflow today; demoting it to build-time config needs its own migration note (Epic F), not a side-effect.muted/dimmed/elevateddon't signal lighter-vs-darker. We're restructuring tokens anyway, so it's the moment — but weigh against the "don't churn names" guardrail; only rename behind an alias + codemod.Epic B: Prop & class renames (breaking · codemod-able)
links/actions→buttons(PageCTA/Empty/Banner/…)input/searchInput→filter(CommandPalette, SelectMenu; feat(DropdownMenu): addfilterprop #6153) — aligns with thefilterprop DropdownMenu/ContextMenu already haveleft/right→start/endprop values (table pinning, chat-messageside, footer, page, chipposition, drawerdirection, slideoverside, toaster position) — the whole RTL story, and the only RTL ask in the thread (alireza-ab, seconded by al1maher);dir, logical theme classes andrtl:mirroring all shipped in v4 already. Breaking twice over: the values rename, and rendering starts following the writing direction (todayside="left"stays visually left underdir=rtl). The ~14 physical classes left insrc/theme/are those values' implementation, so they flip in the same change; the rest stay physical on purpose (centering pairs, Rekatranslate-xanchors). No blanket physical → logical sweep — tailwind-merge keeps them in separate class groups, so it would silently break consumer overrides rather than being additive.tailwindcss(suggestCanonicalClasses)flags and hold new code to it. In-group renames still merge cleanly so they're v4-able as bugs surface, but the cross-group pairs carry the same silent-override break as the logical flip (flex-shrink-0≠shrink-0,overflow-ellipsis≠text-ellipsisin tailwind-merge 3.6.0), so the systematic sweep rides v5. Tailwind drops the deprecated names in its next major anyway, so the v5 floor bump is the natural moment.type: 'label'items — label font size one step smaller than items like the other menus (e.g.text-xsatmdlike Select/SelectMenu, instead oftext-smtoday)NavigationMenuItem<TMeta>/DropdownMenuItem/SelectMenuItemetc. carry a typedmeta/generic instead of[key: string]: any; dropany→unknown. Breaking type-signature change.Epic C: Component restructures (breaking)
base→slots.base([Feature]: Standardize structural slot naming (root, wrapper, container) within the ui prop #6716, closed) — convert the ~14 user-facing single-slot components (Container/Form/Kbd/Link/Main/Skeleton/FieldGroup/Page*/BlogPosts/PricingPlans/ChatShimmer, plus theprose/*set) from a top-levelbasetoslots: { base }, matching Button/Badge. Every component becomes multi-slot, so all of them gain auiprop (single-slot ones only acceptclasstoday) with one consistentapp.config.uishape.root(outer wrapper, 73 components) stays semantically distinct frombase(primary element — they coexist in ~13 components), so we don't merge them. Breaking (app.config.ui.<c>{ base }→{ slots: { base } }, variant valuesstring→{ base: string }); codemod-able.uivalues —{ class, attrs }per slot (Add "pt" props in all components , just like https://primevue.dev/passthrough #6725) — let auislot value be an object carrying arbitraryattrs(aria-*/data-*/…) alongsideclass, not just a class string, so users can set attributes on any internal element without a separate PrimeVue-styleptprop. Reuses the existing slot-key contract (no new API); additive / v4-able (string stays the class shorthand); attributes-only (noon*→ keeps styling ≠ behavior). Cost is broad not deep: every internal element binding routes through a sharedv-bindhelper. Sharp edges: precedence vs component-owned attrs (reservedata-slot/role/Rekaaria-*), and keys that forward to sub-components (leadingAvatar→UAvatar) vs plain elements. Replaces the deferred "richer:ui" note.data-slotwith the component name: every component emits genericdata-slot="root"/"title"/"label"(1029 across 103 files), so[data-slot="root"]matches every component. Adopt the ui.shadcn.com scheme: the root emits the component name (data-slot="card"), parts emit component-prefixed slot names (data-slot="button-leadingIcon",data-slot="card-header"), making each hook globally unique and CSS-targetable per component. The PrimeVue-styledata-component+data-slotsplit (data-pc-name/data-pc-section) is rejected in favor of the single attribute. Single-slotbasecomponents (Container/Main/Kbd/Form/Page*/BlogPosts/PricingPlans/…) gain their root marker here rather than in v4: staticdata-slot="base"additions were tried and reverted in v4 (2026-08-12) because barebaseis un-namespaced and collides in nested subtrees (a FieldGroup or Form wrapper around inputs whose roots all emitbase). Enforce with a DataSlot.spec.ts assertion (bare mount, root carries its component-nameddata-slot) so new components can't drift. Pairs with richeruiabove and the Add "pt" props in all components , just like https://primevue.dev/passthrough #6725 "reach internals" ask. Breaking for bare-name selectors → v5; codemod can't rewrite user CSS, so it ships as a documented rename with find-and-replace patterns ([data-slot="base"]→[data-slot="{component}"]) in the migration guide.sizeonModal/Card) instead of forcing a custom wrapper; aligned across components (dovetails with the Button/Badge size-scale realignment below). Purely-additive one-off variants can still land in v4 as normal feature work; the cross-component consistency is the breaking part.NavigationAccordion(vertical) + mergeContentNavigation; collapsed via CSS transitions (decouple visual vs behavioral collapse, drop manual:collapsedwiring)Dashboardcategory (proposed by @mikenewbon in feat(Sidebar): add resizable functionality #6340) —Sidebar(feat(Sidebar): new component #6038) already covers the core; fold the rest into general components and delete all 10UDashboard*:DashboardSidebar→Sidebargainsresizable(minSize/maxSize/defaultSize/collapsedSize), persisted size/collapsed state (cookie/localStorage) and routeautoClose, built onuseResizable(revisits PR feat(Sidebar): add resizable functionality #6340).DashboardGroup→ gone: shared open/collapsed state moves to auseSidebar()composable (same shared-composable pattern asuseOverlay) exposingopen/collapsed/isMobile+toggle()/collapse()/close()—isMobile+close()because route-only auto-close isn't enough in practice (thecalendartemplate also closes on modal/command-palette open); storage defaults come from<UTheme :props>(feat(Theme): override component prop defaults #6031), and thedashboard:*Nuxt hooks are removed. Covers the old "single root div + provider context rework" item.DashboardPanel/DashboardResizeHandle→ a Splitpanes-style<UResizable>/<UResizablePane>/<UResizableHandle>on a public headlessuseResizablecomposable (min/max, direction, keyboard a11y, persistence); the panel header/body/footer chrome becomes a docs pattern.DashboardNavbar+DashboardToolbar→ one genericToolbar(keeps thetitle/icon/toggleconveniences so the migration is mostly a rename).DashboardSearch+ContentSearch→ one genericSearch(Modal + CommandPalette + shortcut).DashboardSidebarToggle/DashboardSidebarCollapse/DashboardSearchButton/ContentSearchButton→ dropped — they're one-lineUButtonrecipes onuseSidebar()/useSearch(), documented instead of shipped.BlogPosts→PageGridand other thin wrappers get the same review.wrapper= title+description,actionsslot) — PR feat(Sidebar): new component #6038. The props already match (title/description/close/closeIcon+actionsslot everywhere); the remaining gap is layout — Modal/Slideover position the close button absolutely while Drawer/Sidebar put it in an in-flow actions row (Empty div on Slideover when title or description props are not used #5336).Drawerto Reka UI's new Drawer primitive (feat(Drawer): add Drawer primitive unovue/reka-ui#2689 — Vue port of BaseUI Drawer: 4-side swipe, snap points, nested drawers, modal tiers,update:openreason details) and drop thevaul-vuedependency. API shifts (props/emits, snap-point format) → breaking. Dovetails with the unified-header work above and the Reka-base/dep-weight reduction (Epic E).outlinehas bg-default,solidrenders as inverted/dark #6515:outlineshouldn't have bg,solidshouldn't be inverted) — the two are already consistent with each other today (PageCard just addsghost/naked+ orientation/spotlight extras), so this is a redefinition of the shared variants, not a merge.squareelements (equal x/y padding) for icon-only / collapsed-sidebar alignmentsmrenders smaller than Buttonxstoday, forcing customxxsbuttons); net-new sizes are v4-able (normal additive work), the cross-component realignment is the breaking part (see consistent variant coverage above)ColorPickerto Reka'sColorArea/ColorSlider(shipped in reka-ui 2.x, currently hand-rolled) — opens the door to an alpha slider + horizontal hue layout asked for in the thread.useFilter(reka-ui backed) already powers SelectMenu/InputMenu/DropdownMenu; CommandPalette is the last Fuse consumer. Open question: fuzzy scoring + match highlighting, whichuseFilterdoesn't do.ChatMessage/ChatMessagesfrom hardcoded Vercel AI SDK types; ship a mapping helper (AI SDK → Nuxt UI message) + a generic chat base (input/message/messages) enhanceable into the AI chat (reasoning/shimmer/tool/prompt) (Normal chat message component without AI,chat person to person #6598)@tanstack/tablev9 #6799) — migrateTableto@tanstack/vue-tablev9 (9.0.0 shipped 2026-08-04). Internally it's small: exactly one source file imports TanStack (src/runtime/components/Table.vue),useVueTable→useTable, and the four hardcoded row models (core/filtered/sorted/expanded) move into a module-scopetableFeatures()object —getCoreRowModel()is automatic in v9, pagination/grouping stay consumer-supplied viapaginationOptions/groupingOptionsas today.declare module '@tanstack/table-core'just gains theTFeaturesgeneric (declaration merging still works, no need formetaHelper()).TableColumn<T>/TableRow<T>/TableDataare our own aliases, so we supplyTFeaturesourselves and user annotations don't change at all (optionally expose it as a second generic,TableColumn<T, F = typeof features>, so people can register extra features). What makes it breaking isdefineExpose({ tableApi })handing out the raw TanStack instance (40+ call sites across docs/playgrounds/skills): v9 removesgetState()and top-levelonStateChange(→table.store.state/table.atoms.<slice>.get()), renames ~20 pinning methodsleft/right→start/end, splitscolumnSizingInfo→columnResizing, changesgetIsSome*RowsSelected()to mean "at least one" (our indeterminate select-all pattern silently breaks everywhere it appears), and defaultsenableRowRangeSelectionon — shift-range selection as a newUTabledefault is a deliberate call, not a side effect. User column defs change too (sortingFn→sortFn, aggregations →constructAggregationFn()). Not v4-able: no peer range is both non-breaking by default and v9-selectable (peers resolve to the highest satisfying version), andTable.vueships as raw SFC compiled by the consumer'svue-tsc, so with v8/v9 differing in generic arity one file can't type-check against both — dual support means a build-time-swapped second implementation, a third vitest project and duplicated docs, permanently doubling the surface of our most complex component. Payoff: 79% faster core row model, 52% grouping, 37% sorting, 34% filtering, 2.71 GB → 380 MB on 1M rows, tree-shakeable features, plus cell-range selection and native cell spanning (we hand-rollcolspan/rowspanin column meta today). Sweep: 8 direct-importing examples indocs/app/components/content/examples/table/, the Nuxt playground table page, and the REPL (App.vueimport map +vite.config.bundle.tsboth pin the version). Rides Epic B for the pinning value rename and Epic E for the peer move.Contentcategory / own our content types — same move as agnostic Chat above, and the same framework-agnostic driver as Prose × Comark: with v5 leaning on Comark (which works with Vite, not just Nuxt), the Content components must not be Nuxt-specific either. The entire@nuxt/contentcoupling is two imported types (ContentNavigationItem,TocLink) across 6 files, so we define our own (structurally compatible, thin mapping helper for the rest) and drop the@nuxt/contentpeer — it becomes a pure data source:ContentNavigation→ merged into the newNavigationAccordion(see above).ContentToc→PageToc,ContentSurround→PageSurround(codemod renames).ContentSearch/ContentSearchButton→ the genericSearch(its search fn/result types are already ours).Prosecomponents on the@nuxtjs/mdc→ Comark (https://comark.dev) migration. v5 fully embraces Comark, and since Comark works with Vite (@comark/vue) as well as Nuxt (@comark/nuxt), the prose components must carry no Nuxt-specific or@nuxtjs/mdc-specific behavior. They already own their types (zero MDC imports), so it's mechanics:mdc-unwrap→ nativeunwrapslot attr, heading anchor generation drops the deprecatedruntimeConfig.public.mdc.headings.anchorLinksfallback — control moves entirely to the framework-agnosticanchorprop (shipped in v4), which defaults off in the Comark world (Comark has no global anchor config): opt in per heading, or scope a subtree via<UTheme :props="{ prose: { h2: { anchor: true } … } }">/app.config. Module gating swaps@nuxtjs/mdcfor@comark/nuxt(which already auto-registers our prose components), and the deprecatedmdcoption is removed.Editormarkdown support can then ride Comark's planned TipTap extension (first party TipTap extension comarkdown/comark#164).Epic D: Defaults & behavior (breaking)
--ui-container80rem → 90rem (default content width; layout shift)expanddefault →false(stacked)Epic E: Foundation & install contract (breaking)
componentDetectionon by default (Nuxt + Vue) — now that the VuecomponentDetectionport has landed (feat(vue): supportexperimental.componentDetection#6731), flip theme-CSS tree-shaking on by default so apps only ship CSS for components actually used. Breaking-ish: dynamicresolveComponent(name)usage can be missed (escape hatches stay — thecomponentDetection: [...]allowlist, orfalse). Also promote the Vue dev-server HMR re-detection (restart-only in feat(vue): supportexperimental.componentDetection#6731) from "manual-validation follow-up" to a supported path here.@sourcelines — replace the feat(vue): supportexperimental.componentDetection#6731 theme-blanking with the Nuxt mechanism (emit@source "./ui/<component>.ts";per detected component, sharedgenerateSourcespath) once the Tailwind fix ships: Don't scan sibling files of a concrete@sourceinside an auto source root tailwindlabs/tailwindcss#20406 stops a file@sourceinside the Vite root from scanning its whole parent directory (root causeexpand_restricted_patterns, crates/oxide). Gated on the v5 Tailwind floor including that fix — on older 4.x the narrowed list silently produces full CSS, so swapping in v4 would regress everyone below the fix version, which is why blanking shipped in v4 and this waits (decided 2026-08-11). Deletes theunused/applyUnstyledbranch inwriteThemeTemplateand moves Vue detection back intogenerateSources; theme files on disk keep their real classes again (nicer for debugging/inspection). If #20406 is rejected or reshaped upstream, blanking simply stays — it's equivalent in output (~2.4 kB delta from variant keys likeoutlinebeing valid utilities).options.theme.colorsfrom theme files anddata-reduce-motion(A3) removesoptions.theme.transitions, the only two options that 56 of ~120 theme files consume, so theme files become static objects. Components import theme from the package instead of#build/ui/*,writeThemeTemplate+ the.nuxt/ui/node_modules/.nuxt-uicopies disappear (the other half of theUEditorinteraction/input lag caused byUEditorToolbar#6293/Everything gets loaded on a fresh app #6296 dev-startup + HMR churn), and Tailwind scans package sources directly. Synergy with Epic H: static theme objects are stable module-scope identities, so tv factories build once at import (or even publish) time and the WeakMap fast path becomes the default case instead of a calling convention. What stays generated: color-variant keys + prop types (configured aliases) and the scope-class/token CSS intemplates.ts. Needs rework:unstyled(no per-project files left to blank → import swap to a stripped theme export) and the VuecomponentDetectionpath (blanking relied on writing copies; per-file@sourceat package paths still rides the tailwindcss#20406 gate — until then, a generated overlay for undetected components or@source notexclusions). Supersedes the mechanics of the per-file@sourceitem above when both land: the lines point at the package's theme files instead of written templates — same detection, same tailwindcss#20406 gate.useRender(drops the internalPrimitive/asChildwrapper), per-component subpath imports, ESM-only build, plus the perf overhaul and Shadow DOM support. Reka keeps its public component API intact, so it's mostly internal for us; the ESM-only + subpath-import parts touch our build/install, which is why it rides with the v5 cut. Dev-server payoff (Everything gets loaded on a fresh app #6296): the per-family subpath imports (feat(core): remove barrel file in favor of single exports unovue/reka-ui#2764) mean migrating our ~134from 'reka-ui'barrel-import files, and theSymbol.forcontext keys (fix(shared): create context withSymbol.forto fix duplicates unovue/reka-ui#2763) let us droptranspile: ['reka-ui'](src/module.ts, 0.8–1.2s first-request win) — both pulled forward into v4 as small PRs if they ship in a reka-ui 2.x release before the v3 cut.@tiptap/*+ 13 transitiveprosemirror-*, Editor-only), embla (7 pkgs, Carousel-only) and@tanstack/vue-table(Table-only, at^9— see Epic C) fromdependencies→peerDependencies+peerDependenciesMeta.optional.reka-uistays hard dep.@tanstack/table-corestays a devDep (thedeclare moduleaugmentation needs it resolvable at typecheck time) and@tanstack/vue-virtualstays a dep — it's a separate v3 package used by ScrollArea + Table virtualization, andreka-uidepends on it anyway. Also add a@tanstack/*group torenovate.json, there's none today.npm icommand; document per-component install requirements.Table(tableApi, the raw TanStackTable),Carousel(emblaApi) andEditor(editor, the TipTap instance). Document that those objects follow their upstream library's semver, not ours, so an upstream major becomes a Nuxt UI minor with a release note instead of forcing a cut. They're literally third-party objects we forward, and we already tell people to read the upstream docs for them. Reka is the counter-example of why this is narrow: 134 files import from it, but it's fully wrapped, so Reka v3 is internal churn (see above). What this does not cover is a third-party type baked into everyday user code —TableColumn=ColumnDefand the 12 TanStack-typedv-models on Table are ours to version, which is exactly why v9 is still a major (Epic C). So the rule is: wrapped surface = our semver, forwarded instance = theirs. Pairs with the peer move above (a peer at^9already says "you pick the version"). Needs the docs pages for those three props to carry the note, plus a line in the versioning/migration doc.vaul-vueentirely — Drawer moves to Reka's native Drawer primitive (Epic C / feat(Drawer): add Drawer primitive unovue/reka-ui#2689), one fewer external dep on top of the Reka base./vueinstall slugs →/vite(docs-only, non-breaking, v4-able) — the non-Nuxt path is really the Vite plugin (@nuxt/ui/vite+vite.config.ts), not "any Vue setup", so the installation +icons/color-mode/i18nintegration pages read better at/…/vite(pairs as "Nuxt module vs Vite plugin"). Keep the switcher label "Vue" (audience) +framework: vuefrontmatter — only the URL slug changes; ship 301s so old URLs keep working. Catch: the framework value doubles as the slug in the toggle's route-swap watcher (docs/app/pages/docs/[...slug].vue), so it needs a small{ nuxt, vue → vite }slug map; ~75 inbound links to sweep (mostlyicons/vue#themecomponent tips). The switcher label itself stays "Vue" — Vue UI libs organize by framework, not build tool.Epic F: Migration & rollout
app.config.uislot-shape migration ({ base }→{ slots: { base } }) + the TanStack v9 renames (sortingFn→sortFn, pinningleft/right→start/end,columnSizingInfo→columnResizing), with non-fixable warnings fortableApi.getState()and thegetIsSome*RowsSelected()semantics flip. Rides the existing CLI (npx nuxt-ui migrate v5for all transforms,npx nuxt-ui migrate v5/<name> <path>for one) rather than a separate@nuxt/ui-codemodpackage, sincenuxt-ui make componentalready ships there.docs/…/3.migration/1.v5.md— the migration guide itself, following the existing pattern: the new guide takesnavigation.title: 'Migration'+linksback to v4,1.v4.mdrenumbers to2.v4.mdand gainsnavigation: falsewithlinksforward to v5,2.v3.md→3.v3.md. Same source of truth as the codemod and the migrate skill. Plus the Epic A6 token-contract doc +llms.txt. Dashboard traps to capture while both implementations are in-tree:collapsibleis a boolean onUDashboardSidebarand a union onUSidebar, andfalsedoes not map to'none'('none'skips the mobile menu branch entirely and renders inline at every width;falsestill gets a slideover on mobile) ·UDashboardGroup'sunit(templates setrem) needs auseResizableequivalent or persisted sizes silently change meaning · sidebar slot props are a codemod rule, not an alias —v-slot="{ collapsed, collapse }"→{ state, close }withcollapsed→state === 'collapsed', noting the semantic shift (dashboardcollapsed= desktop resize collapse;state === 'collapsed'on mobile = sheet closed) ·dashboard:*hooks: no template uses them, no replacement path needed.migrate-nuxt-ui-v4-to-v5agent skill — the codemod does the mechanical renames, the skill does everything the codemod flags and can't. Distributed the way the AI SDK does it (npx skills add nuxt/ui --skill migrate-nuxt-ui-v4-to-v5, then "migrate my app to Nuxt UI v5"), so it runs in Claude Code / Cursor without us shipping an agent. Scope is exactly the codemod's residue:tableApi.getState()→table.store.state(call-site dependent), thegetIsSome*RowsSelected()semantics flip,[data-slot="base"]selectors in user CSS,:uioverrides pointing at removed token names, and theUDashboard*→Sidebar/Toolbar/Search/useSidebar()restructure with itscollapsibleboolean-vs-union trap. Third surface on the same source of truth as1.v5.mdand theget-migration-guideMCP tool, so all three stay in sync.Epic G: Launch (landing redesign + live theme playground)
main.css+app.config.ts/vite.config.ts. Productizes the Epic A6 theme generator and is the canonical, interactive demo of the token system. Already in the works: docs(theme): add theme studio #6675 (@mikenewbon) prototypes the full studio on top of the docsThemePicker/useThemework — palette bezier-curve editor, semantic shade controls, three font stacks, presets + live preview, minimal-override exports — heading toward a dedicated/themeslayout. It ships without shadow/border axes: they only expressed themselves as static class bundles, so they wait for A2's--ui-shadow-*and A4's--ui-border-width. Headings likewise ride--font-serifas a stopgap until A4's--ui-font-heading. v5 extends it to the new token surface.Epic H: Own the styling engine (tv + merge layer)
Outcome of the tailwind-variants 3.3.x exploration (2026-08-10, prototyped + benchmarked in-tree). Upstream shipped a full internal rewrite as a minor with empty release notes and an 18-103x invocation regression; v4 pins 3.2.2 (
pnpm-workspace.yaml) and our wrapper already re-implements its unexported internals, so we own the semantics either way. v5 stops depending on it.tailwind-variantsbehindsrc/runtime/utils/tv.ts. The prototype (~460 lines) passes the full suite with byte-identical output; fused build+invoke is 8.7x faster than 3.2.2 (the cost every component mount pays), factory build 19x, and its worst case equals 3.2.2's steady state. The surface is small and frozen: no theme usescompoundSlots, responsive variants or per-call config. Landing work beyond the swap: change the call contract to stable identities (tv(theme, appConfig.ui?.button)instead of spreading) so Button-styledefu-injected variants hit the shared WeakMap fast path, delete thewrapSlotsmemo (the engine caches natively and its caches survive factory rebuilds), and own the four type-only imports (ClassValue,TVVariants,TVCompoundVariants,TVDefaultVariants, plusdefaultConfigintemplates.ts).selectorPath::propertysignatures from the user's resolved design system (__unstable__loadDesignSystem+candidatesToCss) in the same pipeline that produces the CSS, so every class that exists as CSS is in the map by construction: theme classes,uiprop overrides, arbitrary values a user types. A class Tailwind never scanned has no CSS, so keeping it verbatim is always safe. Conflict = same properties under the same variant chain, which is whytext-primaryvstext-smnever collide regardless of what tokens are called, where tailwind-merge classifies by heuristic. Prototype: 100% agreement with tailwind-merge over 13,600 merges from real theme strings, exact where it guesses.tailwind-mergestays behind the samecnseam as the fallback for unmapped classes until the map is proven; we do not hand-rewrite the generic library (chasing new Tailwind utilities is a treadmill, see cnfast below). Ship gate: the optimized map (interning/bitmasks; 24.6KB gzip naive) must be worth it against the 17.4KB gzip of tailwind-merge it can eventually remove, measured on the docs app's full scan.Sequencing
unstyledand the VuecomponentDetectionport have landed; further non-breaking precursors land here as they come up).Branching & release
v5branches fromv4, cut only when the first breaking change is ready — Phase 0 stays inv4, so nothing diverges before then.v4; breaking PRs →v5.v4→v5frequently (after eachv4release, or weekly) and never the reverse. Usegit merge, not rebase —v5is a shared branch, so no force-push.v4. Pre-renaming means shipping both spellings, documenting the old one as dead, and carrying the shim until the cut, for a migration the codemod handles in one pass anyway.v4stays the default branch + npmlatestuntilv5is close;v5publishes prereleases (5.0.0-alpha.x) to thenexttag. CI on both. Flip the default tov5near release.colors.ts/templates.ts/src/theme/*, whichv4keeps touching too. Land additive token scaffolding inv4first, keep the breaking theme edits in tight batches, and merge often so conflicts stay small.Open questions (feedback welcome)
Motion tokens — ship in v4 (Phase 0) or with Epic A?Partially shipped in v4 (PR fix(theme): unify motion easing and respectprefers-reduced-motion#6742) — the easing curves (--ui-ease-out/--ui-ease-fluid) + reduced-motion landed in v4 because they're adopted (not inert) and fix a realease-in-on-exit bug; accepted the theme-file churn as the cost. The rest of A3 (named--ui-animate-*, skeleton toggle, tooltip-delay tokens,data-reduce-motionopt-in) stays with Epic A. (Color-roles + surface/overlay + indirection-flatten + container stay v5 regardless.)-foreground— explicit token + defaults vs autooklch(from …)luminance.defaultVariants(surfaced by docs(theme): add theme studio #6675) — theme presets want to flip default variants at runtime, possibly per component group (buttons vs cards vs inputs); todaytheme.defaultVariantsis build-time only.--ui-bgfollow the neutral ramp instead of pure white (also surfaced by docs(theme): add theme studio #6675)? Interacts with the A2 elevation ladders.bg-accentsits next to the existingaccentedneutral token (ring-accented,bg-accented, "stronger neutral"), and Radix Themes usesaccentfor exactly this per-component semantic (data-accent-color). Renameaccentedin the Epic B sweep, or pick another name for the indirection. Leaning: keepaccent(it's what people type most, and the scope classes become reusable public vocabulary —class="accent-error bg-accent-soft"on custom markup) and renameaccented→ e.g.strong.primaryto neutral (black/white until you pick a color) is one line;success/warning/errorkeep their hues. The codemod would insertcolors: { primary: 'green' }for existing apps so upgrades don't shift appearance. Decide with Epic G: shipping colorless with "pick your color" as the first step is the strongest possible demo of the token system. Caveat: everything signaling "on" leans on a chromatic primary (checked/active/selected/focus ring); solid black-white works (shadcn) but dark-mode focus-ring vs border contrast needs the A2 pass. The docs site would setprimary: 'green'explicitly as the demo of the mechanism.Not adopting / deferred
<UDefaults>provider — redundant:<UTheme :props>/<UTheme :ui>already give scoped, deep prop + slot-class defaults via provide/inject. Implicit contextual defaults (auto-restyle based on the ancestor component) intentionally skipped as too magic.--ui-field-*) —app.config.ui.input/<UTheme :ui>already restyle form controls globally and type-safely; a shared "all inputs at once" override is marginal and adds token surface.data-scrollbar— nice polish but orthogonal to the design-token story; could ship as a tiny standalone feature anytime, not a v5 pillar.unstyled+ the(defaults) => classes:uireplace, with Reka UI underneath for full headless control.<dialog>refactor, TSisolatedDeclarations— internal/DX, evaluate separately; not v5 blockers.Issue index
#1284 #6588 #3307 (contrast) · #6608 (neutral-as-primary, closed) · #5657 (scoped theming) · #6515 (variant naming) · #6153 (filter) · #5803 (
:uireplace) · #5336 (close buttons) · #4953 #6169 (per-slot replace) · #6038 (unified headers) · #6551 (unstyled) · #6576 (focus rewrite, shipped) · #4955 (layout components) · #6340 (Sidebar resizable, closed — where de-dashboard was proposed) · #6598 (chat) · #6411 (variant coverage) · #6716 (slot naming, closed) · #6725 (pt→ richerui, closed) · #6675 (theme studio prototype) · #6581 (size scales) · #6293 #6296 (perf) · #6731 (componentDetection Vue port) · #6799 (TanStack Table v9) · unovue/reka-ui#2721 (Reka UI v3) · unovue/reka-ui#2689 (Drawer primitive → drop vaul-vue) · unovue/reka-ui#2763 (Symbol.for contexts) · unovue/reka-ui#2764 (subpath imports) · #6562 (:uireplace fn form, shipped) · #6742 (motion tokens, shipped) · #6031 (<UTheme :props>) · https://comark.dev (Comark)Feedback
Feedback on any item above is very welcome, ideally anchored to a specific one (the open questions especially). Please keep feature requests and new component requests out of this thread, since those aren't tied to a major and can ship in v4. As items firm up I'll spin them into their own issues and PRs.
All reactions