fancy-ui-vue
| Install | |
|---|---|
npm install fancy-ui-vue |
|
| Latest Version: | 0.4.0 |
| Vue: | 3 |
| License: | MIT |
| Last Updated: | Oct 26, 2026 |
| Links: | GitHub · npm |
fancy-ui-vue
Animated UI components for Vue 3.5+, styled with Tailwind CSS v4 — the Vue edition of fancy-ui, ported component for component from the Svelte 5 reference and kept at parity by CI.
Live Demo & Docs · Browse components · Svelte package · React package
Features
- 155 components — core primitives (forms, navigation, overlays) alongside buttons, text animations, backgrounds, cursors, AI/chat building blocks and a micro-interactions collection
- Motion that feels physical — beams of light, liquid-metal and neon borders, a physical flip card, a before/after slider with a motion trail, a datamosh page transition; springs and WebGL where they earn their keep
- Idiomatic Vue 3.5 —
<script setup>single-file components, reactive props destructuring,defineModelfor every bindable (v-model:<name>), snippets as named and scoped slots,defineExposewherever the Svelte source exposes aref; every other prop keeps its Svelte name - Same components, three frameworks — All 155 components of
fancy-ui-svelteare ported, under the same names and with the same visual contract; the Svelte package is the reference,fancy-ui-reactthe React edition, and parity is checked in CI - Tailwind CSS v4 — one CSS import, semantic colour tokens, light and dark themes that follow the OS or a
.darkclass - Accessible and SSR-safe — keyboard support where it applies,
prefers-reduced-motionrespected, aseedprop wherever the source randomises in render so server and client agree, clean hydration under Nuxt, canvas and WebGL work deferred to mount - Opt-in sound — eleven synthesised interface cues, a
SoundToggleswitch and theuseSound/useSoundFeedbackcomposables; silent until the user turns it on - AI-ready docs —
llms.txtwith a Vue section, a Markdown page per component and a JSON registry, so coding agents use the library correctly - Tested — 5,600+ unit tests with Vitest and Vue Test Utils, a Vue 3.5 floor job and a Nuxt consumer app generated in CI
Quick Start
Install via npm:
npm install fancy-ui-vue
Peer dependencies: vue 3.5.2 or later, tailwindcss 4. TypeScript is an optional peer at 5.0 or later.
Add the Tailwind integration and the stylesheet to your app's CSS (e.g. app.css):
@import "tailwindcss";
@import "fancy-ui-vue/tailwind.css";
@import "fancy-ui-vue/styles.css";
tailwind.css tells Tailwind v4 to scan the library's components so all utility classes are generated automatically, and declares the colour tokens the components use; styles.css carries the components' own keyframes and scoped rules. No manual @source path needed.
Then import any component:
<script setup lang="ts">
import { RainbowButton, BorderBeam } from "fancy-ui-vue";
</script>
<template>
<div class="relative overflow-hidden rounded-xl border p-8">
<RainbowButton>Get started</RainbowButton>
<BorderBeam />
</div>
</template>
Props keep their Svelte names; snippets become named slots and every bindable prop is a v-model:<name>. Every deliberate difference is listed under Divergences.
Importing one component from the package root costs the same as importing its own file: the rest tree-shakes away, so there are no per-component subpaths. The four entry points are listed under Package shape.
Or browse and copy a component:
- Find the component you need in the live demo
- Copy the source from
vue/src/components/<slug>/(plus whatever it imports fromvue/src/internals/,vue/src/sound/andvue/src/utils.ts) - Paste into your project
Or clone the full demo locally:
git clone https://github.com/RamaHerbin/fancy-ui.git
cd fancy-ui
pnpm install
pnpm --filter fancy-ui-vue build
pnpm --filter fancy-ui-vue-example-nuxt dev # http://localhost:3000
Copy the source into your project
The npm package stays the canonical way to use these components. If you would rather own the code, every component is also published as a registry item by the docs site, and the ecosystem CLI installs it by URL:
npx shadcn-vue@latest add https://fancy-ui.rama.app/r/vue/rainbow-button.json
The index of all items is at https://fancy-ui.rama.app/r/vue/registry.json. The CLI writes the component folder and the shared modules it needs under a fancy/ folder inside your project's ui alias (components/ui by default), keeping the package's own tree one level deeper so the relative imports stay untouched:
components/ui/fancy/utils.ts
components/ui/fancy/internals/…
components/ui/fancy/sound/…
components/ui/fancy/components/<slug>/…
Five shared items are pulled in automatically as needed: utils, internals, internals-dom, internals-motion and sound. Import from the copied folder:
import { RainbowButton } from "@/components/ui/fancy/components/rainbow-button";
The components carry their own styles (the <style> blocks of the single-file components, plus the CSS files the shared modules import), so the fancy-ui-vue imports from Quick Start are not needed; the components still expect the semantic colour tokens listed under Setup, which a project set up by the same CLI already defines. Dark mode: the components' light-dark() colours follow color-scheme, which that setup does not declare, so add :root { color-scheme: light dark } .dark { color-scheme: dark } to your CSS for them to switch with the dark class. In Nuxt, keep the copied tree out of component auto-import: components: { dirs: [{ path: "~/components", ignore: ["ui/fancy/**"] }] }.
Setup
Tailwind and colour tokens
/* app.css */
@import "tailwindcss";
@import "fancy-ui-vue/tailwind.css"; /* lets Tailwind scan the shipped sources */
@import "fancy-ui-vue/styles.css"; /* keyframes + component-scoped CSS */
Those two imports are the whole setup. tailwind.css also declares every semantic colour the components spend, the standard 18-token semantic set shared by the Tailwind component ecosystem: background, foreground, card, card-foreground, popover, popover-foreground, primary, primary-foreground, secondary, secondary-foreground, muted, muted-foreground, accent, accent-foreground, destructive, border, input, ring. None of them exist in Tailwind's default palette, and Tailwind v4 emits no rule at all for an undeclared colour name, so without them bg-primary and friends compile to nothing. They are wired with @theme inline, so the utilities resolve the variables at use time: an app that already defines that token set keeps its own colours automatically. The defaults ship in @layer base, which an unlayered :root in your app overrides whatever the import order.
Dark mode
Out of the box the package follows the operating-system preference: dark: utilities and the components' light-dark() colours both switch with prefers-color-scheme, so an app that never touches dark mode already looks right on a dark desktop. To control it yourself, put dark or light on <html> (or on any ancestor of the subtree you want to force) and both halves follow the class instead:
<html class="dark">
tailwind.css is what makes that work, in three parts: it declares the .dark token block, it declares a dark variant that matches a .dark ancestor OR the media query (unless a .light ancestor opts out), and it sets color-scheme: light dark on :root with .dark / .light overrides so the components' light-dark() colours — and the browser's own form controls, scrollbars and caret — resolve the same way. An app that declares its own @custom-variant dark after the import keeps its own definition.
Nuxt
The setup above works as-is in Nuxt (add @tailwindcss/vite to vite.plugins). A Nuxt module, fancy-ui-nuxt, auto-imports every component as <Fancy…>, adds styles.css and auto-imports the sound composables; it lives in this repo and is not yet published to npm.
Server rendering
The package is written for SSR: the examples/nuxt-app consumer in this repo is generated in CI with failOnError on, so a component that breaks server rendering or hydration fails the build. Three rules follow from that and are worth knowing:
- Nothing portalled is emitted on the server. Dialogs, sheets, drawers, popovers, tooltips, menus, pickers and the toaster render their trigger only; the panel paints on the first post-hydration patch, whether it starts closed or open by default.
toast()is a no-op outside the browser. - Render-path randomness is seeded. Components that scatter stars, shuffle colours or scramble characters take a
seedprop (default1), so the server and the client draw the same picture; varyseedto desynchronise two instances. - Browser-only state lands after mount. Sound preferences hydrate in
onMounted, relative timestamps and reduced-motion queries start one tick after mount, and generated ids come from Vue'suseId()so they match across the server render and its hydration.
The details, component by component, are under Divergences.
Package shape
Three things about the package's shape, because each of them is a decision rather than an oversight:
- ESM only. There is no
requirecondition and no CJS build. A TypeScript project onmoduleResolution: node16that emits CommonJS gets TS1479 on the bare import; the fix is on that side ("module": "esnext", or a dynamicimport()), not here. - Four entry points, no deep imports.
fancy-ui-vue,fancy-ui-vue/cameleon,fancy-ui-vue/styles.cssandfancy-ui-vue/tailwind.css, plusfancy-ui-vue/package.jsonfor tooling that resolves a manifest by specifier. Everything else is blocked by the exports map on purpose: the build keeps one module per source file, so importing one component from the package root tree-shakes the rest away, and the alternative would freezedist/'s internal folder layout as public API. - 0.x, so minors can break. Until 1.0 a minor version may change a component's API; patches never do. Every deliberate difference from the Svelte package is listed under Divergences.
Compatibility
- Vue 3.5.2 or later. 3.5 is the floor, not just the minimum peer range: the port relies on reactive props destructuring,
useTemplateRef,onWatcherCleanupand<Teleport defer>. A CI job re-installs the workspace with Vue pinned to 3.5.2 and runscheck,test,buildand a consumer script against it. - TypeScript 5.0+. Below it, the package's own declarations fail to parse and everything from it reads as
any. - Tailwind CSS 4.
tailwind.cssuses v4 at-rules (@source,@theme inline,@custom-variant) and has no v3 equivalent. - The cameleon subpath is barrel-only.
fancy-ui-vue/cameleonships the skin engine, the primitives and the five skins. There is no./cameleon/*wildcard for a single skin, and the retro-OS component kit that the Svelte package publishes asfancy-ui-svelte/cameleon/retro-kithas no Vue counterpart.
Using with AI agents (Claude Code, Cursor, Copilot)
The docs site serves documentation built for coding agents, following the llms.txt convention:
- fancy-ui.rama.app/llms.txt — setup guide, usage rules, a Vue section and an index of every component
- fancy-ui.rama.app/llms-full.txt — full reference: import, props and a working example per component
https://fancy-ui.rama.app/docs/components/vue/<slug>.md— one component as Markdown, written for Vue: the names the Vue port takes (models, listeners, slots), its props and its Vue example when there is one (e.g. vue/compare.md); each docs page also has a Copy as Markdown button- fancy-ui.rama.app/registry.json — every component as JSON: for each framework (Svelte, React, Vue) the import names, Markdown page, copy-source item and availability, plus props, snippets and links
To make your coding agent use fancy-ui correctly, add this to your project's CLAUDE.md, AGENTS.md, or .cursorrules:
## UI components
Use fancy-ui-vue (Vue 3.5 + Tailwind CSS v4) for animated UI components.
Setup and rules: https://fancy-ui.rama.app/llms.txt (see the Vue section)
One component (props + examples): https://fancy-ui.rama.app/docs/components/<slug>.md
Key rules: import "fancy-ui-vue/styles.css" once; bindables are `v-model:<name>`
(no bare `v-model`); snippets are slots; overlay/effect components (BorderBeam,
GlowBorder, backgrounds, ...) need a parent with `relative overflow-hidden`;
cursor effects (FluidCursor, SmoothCursor) mount once in the root layout.
Development
Workspace member of the repo's pnpm workspace — one pnpm install at the repo root covers it.
pnpm install # at the repo root
pnpm --filter fancy-ui-vue test # vitest + vue test utils
pnpm --filter fancy-ui-vue check # vue-tsc --noEmit
pnpm --filter fancy-ui-vue check:matrix # migration matrix held to the Svelte reference
pnpm --filter fancy-ui-vue build # vite lib build + d.ts via vue-tsc, then the dist gates
pnpm --filter fancy-ui-vue test:scripts # the gates' own regression tests (needs a build)
pnpm --filter fancy-ui-vue-example-nuxt dev # the consumer app (examples/nuxt-app) on http://localhost:3000
pnpm check:props # at the repo root: every Svelte prop has a React and a Vue counterpart (CI gate)
pnpm check:cores # at the repo root: shared framework-free engines are byte-identical in the three packages (CI gate)
The consumer app under examples/ is nuxt-app, which resolves fancy-ui-vue to vue/dist, so run build first. CI runs nuxt generate on it as the SSR/hydration gate: failOnError in its prerender block is what makes a broken page fail the build instead of being skipped. A second CI job, vue-3-5, re-installs the workspace with a job-local override pinning Vue to 3.5.2 (the peer floor) and runs check, test, build and scripts/vue-floor-consumer.mjs against it.
build does more than build. After Vite and vue-tsc it runs six scripts in vue/scripts/, each guarding something no other gate can see:
| Script | What it stops |
|---|---|
strip-dts-css-imports.mjs |
vue-tsc copies each component's import "./x.css" into its .d.ts, naming a file dist does not ship — one TS2307 per component for any consumer without skipLibCheck |
check-dist-names.mjs |
a minified build shipping AnimatedBeam as const ut = ... in every consumer's devtools tree and stack traces |
prune-orphan-dts.mjs |
a declaration under dist/internals/ with no runtime module behind it, typing an import that cannot load |
check-dist-shape.mjs |
a test rig reaching the tarball, a declaration pointing at a file that is not there, an SFC without <script setup>, a bare import of a package nobody declared |
smoke-dist.mjs |
the built artifact never being executed: it imports every module with no DOM, server-renders each component export, and holds a gzip budget over the emitted JS and CSS |
ensure-styles-css.mjs |
the ./styles.css subpath being declared while no stylesheet was emitted |
test:scripts runs dist-gates.test.mjs, which tests the gates' own logic (what counts as a component, what the sweep counts) so a gate cannot go quiet without a test noticing. check-matrix.mjs (check:matrix) holds the migration matrix to the Svelte reference, and vue-floor-consumer.mjs is the CI-only floor check above; neither is part of build.
The output is not minified, for the reason check-dist-names.mjs exists: the consumer's bundler minifies over the whole app, and minifying here costs every component its name.
Project Structure
vue/
├── src/
│ ├── components/ # UI components (one folder per component, same slugs as the Svelte source)
│ │ └── rainbow-button/
│ │ ├── RainbowButton.vue # <script setup> + <style scoped>
│ │ ├── RainbowButton.test.ts
│ │ └── index.ts
│ ├── internals/ # Shared framework-free engines and Vue composables
│ ├── sound/ # Sound engine, themes, SoundToggle and the composables
│ ├── cameleon/ # Skin engine: FancyProvider, primitives, skins (fancy-ui-vue/cameleon)
│ ├── utils.ts # cn()
│ └── index.ts # Barrel exports
├── scripts/ # Dist gates and their tests, migration matrix check, Vue floor consumer
├── tailwind.css # Tailwind v4 integration shipped to consumers
├── PORTING.md # The law every port follows
├── internals-api.md # Contract of the shared internals
└── migration-matrix.json # Per-component parity record against the Svelte reference
examples/nuxt-app # Nuxt consumer generated in CI (SSR/hydration gate)
Component tests are colocated with their component; there is no separate unit-test directory. The Svelte reference for every component lives at src/lib/fancy-ui/<slug>/ at the repo root.
Contributing
Contributions are welcome. A component exists in this package because it exists in the Svelte reference, so the flow starts there:
- Implement or change the component in
src/lib/fancy-ui/<slug>/(Svelte 5), with its tests - Port it to
vue/src/components/<slug>/followingPORTING.md— folder shape, rune→composable mapping, the styling rules that break silently, and the hard rules; the Svelte source is always the reference - Record any deliberate difference under Divergences and update
migration-matrix.json - Run the package gates (
check,test,build,check:matrix,test:scripts) and the repo gates (pnpm check:props,pnpm check:cores) — CI fails on a missing port or a missing prop - Add a changeset (
pnpm changeset)
The full walkthrough is in CONTRIBUTING.md.
Divergences from the Svelte API
Everything not listed here is a faithful transpose of the Svelte reference. The first list states each package-level rule once, with the components and names it touches; the second holds only what is specific to one component. Components are named by their exported Vue name.
Package-level rules
- props:
<Name>Propshold the component's own props only, never aButtonHTMLAttributes-style intersection or Svelte'sOmit<HTMLAttributes<…>, …>fold-in, so native attributes are untyped. Every component setsinheritAttrs: falseand writesv-bind="attrs"only where the Svelte source spreads{...restProps}, at the same position, so attributes the component sets after it (data-state,data-position,data-panel-side) still win.- These components forward native attributes and listeners (rules in PORTING.md): Autocomplete, BentoGrid, BentoGridCard, BentoGridItem, Book, BookDescription, BookHeader, BookTitle, BoxReveal, Button, Checkbox, Combobox, ComposerInput, ComposerModelPicker, ComposerSubmit, ConfettiButton, ContainerTextFlip, CopyButton, DatamoshTransition, DatePicker, DimSiblings, DropdownMenuItem, DropdownMenuSubTrigger, DropdownMenuTrigger, FileUpload, GradientButton, IconButton, InlineCitation, Input, InteractiveHoverButton, LetterPullup, LineHoverLink, Link, MagnetPuck, Magnetic (outer wrapper), MagneticField, MagneticItem (outer wrapper), MosaicGlow, NavbarLink, NavigationMenuLink, NavigationMenuTrigger, NeonBorder, NumberInput, PasswordInput, PixelLoader, Popover, Presence, Pressable, PulseBeam, RadioGroup, RadioGroupItem, RainbowButton, Reveal, RippleButton, ScrollProgress, SearchInput, Select, ShimmerButton, Skeleton, Slider, SourcesTrigger, StatusMorph, StickyScroll, Switch, TabsTrigger, TerminalBlock, Textarea, TextRoll, TimePicker, Toggle, ToggleGroupItem, TypingIndicator. Button, CopyButton and IconButton land them on the
<button>(Button and IconButton on the<a>oncehrefis set). Capture-phase listeners (@click.capture) are stripped while Button or IconButton is disabled or loading; the Svelte and React ports do not guard capture-phase listeners. Targets: Input, Textarea, PasswordInput, SearchInput and NumberInput → inner<input>/<textarea>; Link, LineHoverLink, NavbarLink, NavigationMenuLink and InlineCitation → their anchor (InlineCitation's marker is a<button>when unlinked), where the managedaria-current,aria-disabled,tabindex,target,reland label win while they apply; Book, BookHeader, BookTitle and BookDescription → their own element; the other display components (BoxReveal, BentoGrid, BentoGridItem, BentoGridCard, ContainerTextFlip, LetterPullup, NeonBorder, PixelLoader, TerminalBlock, TypingIndicator) → their root, where managedrole,aria-liveanddata-statewin and a consumerstylemerges; Checkbox, Switch, Slider, RadioGroup, RadioGroupItem and FileUpload → their native input or group, where@change/@inputare declared camelCaseonChange(Slider alsoonInput); Select, Combobox, Autocomplete, DatePicker and TimePicker → trigger or input, with the component's invariants merged after the attributes. Triggers and items: DropdownMenuTrigger, DropdownMenuItem, DropdownMenuSubTrigger, TabsTrigger, NavigationMenuTrigger, SourcesTrigger, Toggle, ToggleGroupItem, Popover (trigger button;classandrefstill target the panel) and ComposerSubmit and ComposerModelPicker → their button, ComposerInput → textarea; they declare camelCaseonClick,onKeydown,onFocus,onMouseenterandonMouseleave(skipped while disabled), and managedrole,type,aria-expanded,aria-controls,aria-selected,aria-pressedand rovingtabindexwin. A consumerstylemerges with the component's own custom properties (mergeProps) instead of overwriting them the way a later{...restProps}would. - Every component not listed above is closed, exactly as in Svelte: an
id,data-*,aria-*or@clickput on it is dropped silently (<Dialog id="x" data-foo="bar">renders neither), which differs from Vue's default fallthrough.
- These components forward native attributes and listeners (rules in PORTING.md): Autocomplete, BentoGrid, BentoGridCard, BentoGridItem, Book, BookDescription, BookHeader, BookTitle, BoxReveal, Button, Checkbox, Combobox, ComposerInput, ComposerModelPicker, ComposerSubmit, ConfettiButton, ContainerTextFlip, CopyButton, DatamoshTransition, DatePicker, DimSiblings, DropdownMenuItem, DropdownMenuSubTrigger, DropdownMenuTrigger, FileUpload, GradientButton, IconButton, InlineCitation, Input, InteractiveHoverButton, LetterPullup, LineHoverLink, Link, MagnetPuck, Magnetic (outer wrapper), MagneticField, MagneticItem (outer wrapper), MosaicGlow, NavbarLink, NavigationMenuLink, NavigationMenuTrigger, NeonBorder, NumberInput, PasswordInput, PixelLoader, Popover, Presence, Pressable, PulseBeam, RadioGroup, RadioGroupItem, RainbowButton, Reveal, RippleButton, ScrollProgress, SearchInput, Select, ShimmerButton, Skeleton, Slider, SourcesTrigger, StatusMorph, StickyScroll, Switch, TabsTrigger, TerminalBlock, Textarea, TextRoll, TimePicker, Toggle, ToggleGroupItem, TypingIndicator. Button, CopyButton and IconButton land them on the
- boolean props: a DOM attribute the component reads is a declared Boolean prop rather than a value read off
$attrs, so a bare<RippleButton disabled>istrue, not the falsy string""that would let the sound cue play:disabledon GradientButton, InteractiveHoverButton, RippleButton, ShimmerButton. Vue's Boolean casting applies to every Boolean prop:<ChatMessageAction active="">istruewhere Svelte rendersaria-pressed=""(activekeeps three states through an explicitundefineddefault), and ReasoningPanel's bareopenistrue. - class:
classis a declared prop typedHTMLAttributes["class"](string, array or object) rather thanstring, merged once bycn()at the Svelte call site and argument position and never doubled through$attrs. Exceptions: ReviewCard (Marquee) declares noclass, as in Svelte, so aclasson it is dropped, and the renderless roots ContextMenu, DropdownMenu and DropdownMenuSub have none; LiquidGlass keepscontainerClassastring; FrostedGlass'sclassstill targets the content layer andcontainerClassthe outer container. - ref: Svelte's
ref = $bindable(null)becomesdefineExpose({ ref })on the same node.refis a reserved vnode key, so it is absent from every<Name>Propsand there is nobind:reforv-model:ref: put a template ref on the component and read.ref(wrapper.vm.refin tests).- Nodes worth knowing: Button, CopyButton and IconButton expose whichever of
<a>/<button>hrefselected (CopyButton and IconButton forward Button's ownref); Popover re-exposesPopoverContent's panel; Select, DatePicker, TimePicker and Tooltip expose the trigger; Checkbox, Combobox, Autocomplete, NumberInput, Slider (<input type="range">) and RadioGroupItem the native<input>; Textarea the<textarea>; Magnetic its static outer wrapper; Toast.ft-toastand Toaster.ft-toaster; Composer's parts the form, the textarea and the menu div; NavigationMenu, NavigationMenuList, NavigationMenuTrigger and NavigationMenuContent (NavigationMenuItem and NavigationMenuLink expose none, as in Svelte). nullwhile nothing renders: Presence and ComposerCommandMenu while closed, Skeleton in standalone mode withloading=false.- Confetti also exposes
fire():const c = useTemplateRef('c'); c.value.fire()replacesbind:thisplus Svelte'sexport function fire. - DatamoshTransition also exposes
cover(),reveal()andplay():const t = useTemplateRef('t'); await t.value.cover()replacesbind:thisplus Svelte'sexport functions.
- Nodes worth knowing: Button, CopyButton and IconButton expose whichever of
- slots: snippet props become slots, typed with
defineSlotsand absent from<Name>Props.childrenbecomes the default slot (required where Svelte'schildren: Snippetis: BoxReveal, DimSiblings, Magnetic, Presence, Pressable, Reveal, ScrollAnchor), every other snippet a slot of the same name, and{#if x}guards becomev-if="$slots.x", so optional wrappers are still omitted when a slot is absent. A<slot>fallback also renders when a SUPPLIED slot renders nothing (ChatError with an empty default slot shows its built-in message), where Svelte's{#if children}would render an empty body.- Named slots: AlertDialog
#trigger; ArtifactCard#actions; BentoGridItem#header#icon#title#description, BentoGridCard#icon#background; Button#iconStart#iconEnd; ChatEmptyState and ChatError#icon; ChatMessage#avatar#actions#footer(its default slot still replaces the body and makescontentignored); ChatPanel#header#composer#emptyState; CommandMenu, ComposerCommandMenu and ThreadList#empty; Compare#firstContent#secondContent#handle(an empty#handlesuppresses the handlebar); Composer#accessory; ContainerScroll#titleContent#cardContent; Dialog#footer#trigger; Drawer and Sheet#footer; DropdownMenuItem and DropdownMenuSubTrigger#icon; Markdown#trailingCursor(StreamingText fills it while streaming); Navbar#brand#actions; Pagination#previousLabel#nextLabel; Popover#trigger; SidebarItem#icon, SidebarFooter#avatar; SmoothCursor#cursor; SourceCard#icon; StatusMorph#idle; TerminalBlock#header; TextRevealCard#text#revealText; ThinkingIndicator#done; ToolCall#icon. - Scoped slots, where Svelte's positional
Snippet<[A, B]>arguments arrive as one named object: AgentPlan#item="{ step, index }"; AiDataTable#cell="{ value, at }"withat={ row, key }; Breadcrumb#item="{ item, index }"(still replaces every crumb); CommandMenu#icon="{ item }"; HoverCard#trigger="{ descriptionId }"; InlineCitation#preview="{ source }"; PromptSuggestions#item="{ suggestion, index }"; SourcesList#item="{ source, index }"; StickyScroll required#item="{ item, index, active }"and#panel="{ item, index }"; SubagentList#item="{ agent, index }"; ThreadList#item="{ thread, active }"; Timeline#content="{ item }"; ToolCall#input="{ value }"and#output="{ value }"; ToolTimeline#item="{ item, index }"; WebSearch#item="{ result, index }". - Svelte's
{:else}content is the slot fallback: InteractiveHoverButtontext, RadioGroupItem and ToggleGroupItemlabel ?? value, Pagination‹ Previous/Next ›.
- Named slots: AlertDialog
- v-model: each Svelte bindable is a model of the same name, written
v-model:<name>. There is nomodelValuealias, so a barev-modelbinds nothing. Every model is declared bydefineModel. Most also stay listed, typed, in the exported<Name>Props; nine are declared bydefineModelalone and are absent from it: Checkboxchecked/indeterminate, RadioGroupvalue, StatusMorphstate, Steppercurrent, Switchchecked, Tabsvalue, Togglepressed, ToggleGroupvalue. The documented call shapes all still work — two-way, theonXChangecallback alone, or a plain value plus callback — and the callback fires after the model write, in Svelte's order.v-modelon native inputs defers to IME composition, as the input element does.v-model:open: AlertDialog, CommandMenu, ContextMenu, Dialog, Drawer, DropdownMenu, HoverCard, Popover, ReasoningPanel (tri-state:undefineduntil someone decides), Sheet, Sources, ToolCall.v-model:value: Autocomplete, Combobox, Composer, ComposerModelPicker (no default, so an unset value falls back to the first model), DatePicker, Input, NavigationMenu, NumberInput, PasswordInput, RadioGroup, SearchInput, Select, Slider, Tabs, Textarea, TimePicker, ToggleGroup.v-model:checked: Checkbox, Switch;v-model:phase: DatamoshTransition (output-only, see below);v-model:indeterminate: Checkbox;v-model:state: ApprovalCard, RecommendationCard, StatusMorph;v-model:pressed: Toggle;v-model:current: Stepper;v-model:page: Pagination;v-model:files: FileUpload;v-model:attachments: Composer;v-model:collapsed: CodeDiff;v-model:activeIndex: StickyScroll;v-model:activeId: ThreadList;v-model:active: VoiceInput;v-model:query: CommandMenu.- Passing the value AND an
@update:<name>listener hands ownership to the parent, where Svelte's bindable always writes locally. A listener that never writes the value back leaves Dialog open (data-statestaysopenafteronOpenChange(false)), Pagination unmoved, and ApprovalCard and RecommendationCard pending with live buttons (a one-tick latch still resolves a decision exactly once, even for two clicks in one tick). The rendered state never disagrees with the model, though: after a press the parent declined, Switch and RadioGroup put their native controls back on the model in the next tick (soaria-checked, the visible track and the callbacks all describe the model), and VoiceInput's start guard follows the model rather than a latch of its own. A value passed with no listener still drives itself. Exception: NavigationMenu keeps an eager internal mirror ofvalue, so it moves even whenupdate:valueis ignored.
- callbacks: callback props stay props, called as
props.onX?.()rather than emitted, so devtools shows no event pane for them. The compiler still turns a listener spelling into the prop:@value-change,@open-change,@files-change,@stick-change,@toggle,@select,@enter-end/@exit-endreachonValueChange,onOpenChange,onCheckedChange(Checkbox, Switch),onCurrentChange(Stepper),onQueryChange(CommandMenu),onFilesChange(FileUpload),onStickChange(ScrollAnchor),onToggle(ToolCall),onSelect(PromptSuggestions, called with(suggestion, index)) and Presence'sonEnterEnd/onExitEnd. AppleCard'sonExpand/onCollapseare passed as:on-expand/:on-collapse.@click(the camelCaseonClickprop) is a declared listener on Button, ChatMessageAction, CopyButton (no lowercase alias; it runs right after the copy starts), GradientButton, IconButton (forwarded to Button), InteractiveHoverButton, Link, NavbarLink, RippleButton and SidebarItem, called exactly once per activation and still behind the component's existing guards and press cue. Link, NavbarLink and the other anchor components pass their other native listeners (@focus,@keydown, …) through to the anchor too. The lowercaseonclickprop (:onclick="fn") stays as a deprecated alias, called only whenonClickis absent. A closed component without a declared listener still drops@clickwith the other attributes.- Exceptions riding in
$attrs: ShimmerButton'sonclickis plain@clickfallthrough, merged after the press cue in Svelte's order; ShimmerButton'sonpointermoveis likewise plain@pointermovefallthrough, merged after the component's own pointer tracking (which writes --mx/--my), as in Svelte; Reveal'sonfocusinis a caller's@focusin, fired after the component's own handler.
- context keys: exported context keys are Vue injection keys, not bare
unique symbols forsetContext/getContext; the context value shapes are unchanged.- Helper objects built by
createInternalContext(.key,.provide(),.useRequired(),.useOptional()):BUTTON_GROUP_CONTEXT(Svelte's bareBUTTON_GROUP_CONTEXT_KEYsymbol has no Vue export under that name),CONTEXT_MENU_KEYwith the re-exportedMENU_KEY/SUB_KEY,NAVIGATION_MENU_KEY/NAVIGATION_MENU_ITEM_KEY,STEPPER_KEY,TABS_KEY,TOGGLE_GROUP_KEY. A hand-written leaf callsMENU_KEY.useRequired()instead ofgetContext(MENU_KEY). - Plain
InjectionKeysymbols, read withinject(KEY, undefined):AUTOCOMPLETE_KEY,COMBOBOX_KEY,POPOVER_KEY,RADIO_GROUP_KEY,SIDEBAR_KEY,SOURCES_CONTEXT_KEY. - Outside its provider, a required part throws a NAMED error instead of reading
undefined: the Autocomplete panel,PopoverContent(used standalone it now throws rather than rendering an unanchored panel), NavigationMenuList / Trigger / Content and the menu leaves. Optional parts degrade instead: Sidebar parts (collapsed: false), Sources parts, RadioGroupItem, and — where Svelte throws a TypeError — CardItem outside aCardContainer(Svelte calls thecard3d:mouseEnteredgetter unguarded; Vue renders the rest transform,translateX(0px) … rotateZ(0deg)) and Dock's icons and separators (a frozenmagnify: false,orientation: "horizontal"fallback: a lone icon renders at its resting 40px).
- Helper objects built by
- surfaces: there is no
markSurfaceState.data-stateis an ordinary binding of the presence clock's two values,"open"/"closing"(never"opening"), andinertduring an exit is written by the presence clock as an ATTRIBUTE (toggleAttribute), not the IDL property::not([inert])selectors and assistive tech see it everywhere, but under jsdom asserthasAttribute("inert"), notel.inert. AlertDialog, Autocomplete, CommandMenu, ContextMenu, DatePicker, Dialog, Drawer, DropdownMenu, HoverCard, NavigationMenu, Popover, Toast; AnimatedTooltip and DirectionAwareHover run their exits on the same clock instead of a Svelte transition such astransition:scale(same timing and easing; the tooltip node appears one extra pre-paint render pass after the hover); Presence withinert={false}never writes the attribute at all. Collapsed regions carry a present-or-absentinertattribute: ReasoningPanel's trace, SourcesList (<ul inert>, neverinert="false"), CodeDiff's body.- Autocomplete, Combobox and Popover render their panel part unconditionally and let it own the mount gate, where Svelte writes
{#if open}one level up. A closed surface still emits nothing, on the server included (Combobox leaves a pair of empty fragment comments).
- Autocomplete, Combobox and Popover render their panel part unconditionally and let it own the mount gate, where Svelte writes
- portal:
<Portal>(a<Teleport>) sits INSIDE the presence-mounted gate, the inverse of the React package's ordering: a closed surface emits nothing server-side, and the teleport attaches its children during the patch that creates them, so a focus trap never focuses a detached node (CommandMenu, Combobox, NavigationMenu and every portalled surface).- Nothing portalled is emitted on the server, open OR closed:
<Portal>renders nothing on the server and on the hydration pass and portals synchronously on any later mount, so every portalled surface (Dialog, AlertDialog, Sheet, Drawer, Popover, Tooltip, HoverCard, Toaster, the menus and pickers) behaves the same whether it starts closed or open by default — server HTML holds the trigger only, the panel paints on the first post-hydration patch, and hydration is clean. The same contract as the React package; Svelte emits an open surface inline and relocates it on mount.toast()stays a no-op outside the browser. - Always-mounted nodes that Svelte portals from a
use:portalaction render in place and relocate todocument.bodyone flush AFTER mount, as a new node rather than the moved original: ContextMenu's.ft-context-menu-anchorspan (<Portal :disabled>flipped inonMounted) and StatusMorph's<div role="status">live region. Code readingdocument.body.querySelector(…)synchronously after mount mustawait nextTick()first.
- Nothing portalled is emitted on the server, open OR closed:
- timing: DOM-reading watchers run with
flush: 'post', and Svelte's mount-time$effectbecomesonMounted(animmediatewatcher would run during server setup), so a few things land one tick after mount — still before paint, but a test mustawait nextTick():- IntersectionObserver attachment: BoxReveal, Reveal, NoiseReveal (
trigger="view"), StickyScroll rows (activeIndexcannot move before the first tick). - Roving tab stops: Tabs and ToggleGroup render
tabindex="-1"on every item on the server and the first client render, then patch in the0. - Stepper registration: step numbers,
data-status,aria-currentand connectors repaint on the next tick; the<li>count is right immediately. - Auto-open: ReasoningPanel mounted mid-stream, ToolCall mounted with an already-failed call (after hydration when
openis bound from outside). - PromptSuggestions'
$effect.preis aflush: 'pre'immediate watcher that runs in setup, before the first render (inert by construction).
- IntersectionObserver attachment: BoxReveal, Reveal, NoiseReveal (
- ids: generated ids come from
useFancyId()(Vue'suseId()) instead of$props.id()orMath.random(), and readv-0,v-1, …: Dialog and Drawerv-0-title/v-0-description, Selectv-N-listbox/v-N-option-K, Tabsv-0-trigger-account/v-0-panel-account, ToolCallv-0-header/v-0-body, DropdownMenuv-N-content/v-N-trigger, FormFieldv-0-control(same<base>-label/-description/-errorsuffixes, and an explicitidyields identical ids), plus AnimatedBeam's gradient and filter ids (beam-v-N,beam-v-N-glow/beam-v-N-bloom), ContextMenu, ReasoningPanel headers, Sheet and SidebarGroup headings. They are stable across a server render and its hydration and serve only asaria-*targets: nothing may depend on the shape or use it as a CSS selector. Ids minted after mount come from an incrementing client-only counter: FrostedGlassfg-dist-<n>, LiquidGlasslg-1,lg-2, …. RadioGroup's generatednamehas its own bullet below. - seeds: render-path
Math.random()is replaced by aseedprop (default1) feeding a mulberry32 stream identical to the React package's, so a server render and its hydration agree. Two instances with noseednow look alike; varyseedto desynchronise them. ColourfulText (Fisher-Yates over the stream instead of a biasedsort, so the exact order differs slightly); Compare'sStarField; HyperText (scramble characters, seeded in setup so the server render emits them); SparklesText (streamsseedandseed + 1; sparkle ids no longer includeDate.now()); StarsBackground (the ~1600-entry box-shadow sky is in the server HTML, with no first-frame starless flash; the sameseed+starColornames the same sky as React); TextRevealStarsseedand TextRevealCardstarsSeed(forwarded; unset inherits 1); No seed where randomness runs only after mount: BgFallingStars, FlickeringGrid. - clock:
createNowrenders an empty sentinel label on the server and through hydration and starts inonMounted(a wrong clock value would otherwise survive Vue's production hydration), so relative timestamps appear one tick after mount where the Svelte package paints them immediately: ChatMessage (the<time>, and the row holding it when no#actionsis given), ThreadList, ToolTimeline.datetime/titlecarry the exact time from the first render. DatePicker'sviewDate/focusedDatelikewise staynulluntil the first open instead ofnew Date()in setup. - styles:
<style scoped>replaces Svelte's scoping, with four visible consequences.- Scoped keyframes are renamed (ArtifactCard's
ft-artifact-sweepbecomesft-artifact-sweep-<scope-id>, likewise AgentPlan and ApprovalCard): the animation is unaffected, but a consumer cannot redefine a keyframe by its source name; the documented custom properties (--ft-artifact-sweep-duration, …) are the override surface. - Keyframes referenced from an inline style or a custom property ship unscoped under their source names, from a second unscoped
<style>block (or an all-unscoped one where nothing else needs scoping: ContainerTextFlip, LetterPullup): FlipWords (flipFadeInWord,flipFadeInLetter,flipEnterWord,flipExitWord), LineShadowText (line-shadow), TextRevealCard (star-drift), RainbowButton (@keyframes rainbowand.animate-rainboware global, so a consumer's ownrainbowkeyframe or class collides). A parent's scoped selector can also still match a child component's root element. - Svelte's
:global(…)descendants become:deep(…): BlurReveal:deep(*), Reveal:deep(*:not([data-reveal-skip])). Overriding Magnetic's.ft-magnetic::beforehalo from a consumer's<style scoped>needs:deep(). - Inline custom properties are bound as a style object, never a string (a string goes through
cssText, where jsdom drops custom properties), so the server-rendered attribute has no space after the colon (style="--rainbow-speed:2s;",--shimmer-color:#ffffff;,--blur-reveal-duration:1s;,touch-action:none;). The parsed value is identical and hydration is clean: BlurReveal, BorderBeam, CopyButton, GlareCard, GlowBorder, GradientButton, ImageTrailCursor, LineShadowText, LiquidGlass, RainbowButton, ShimmerButton.
- Scoped keyframes are renamed (ArtifactCard's
- transitions: Svelte
in:/out:directives become<Transition :css="false">/<TransitionGroup :css="false">driving the sharedrunTransitionsampler, with the same curves and durations.@vue/test-utilsstubs both by default, which silently removes the motion and theinertexit window; a test that needs them passesglobal: { stubs: { transition: false, 'transition-group': false } }. Button (spinner ↔iconStartcross-fade), FileUpload rows, FormField messages, SearchInput's clear button, StickyScroll's panel crossfade, Tabs' panel entrance, TextRoll cells, Toast / Toaster. - reduced motion:
prefers-reduced-motionis read through the shareduseReducedMotion()composable instead of an inlinewindow.matchMedia(...), with the same query and the same mount-time answer: FireworksHdr, NoiseReveal, SmoothCursor (a genuine change is still routed throughsetOptions({ reducedMotion })). FireworksHdr keeps an unreadchangelistener attached for its lifetime. - sound cues: cues go through
useSoundCue, which always forwards its options argument, so the controller seesplay("select", undefined)where Svelte callsplay("select"). Audibly identical; only a spy asserting exact arguments notices: AnimatedTestimonials, ArtifactCard and RainbowButton (press), Breadcrumb, Navbar, Pagination, PasswordInput, RecommendationCard (select/close), WebSearch (select/open/close). - markup: Vue's template whitespace condensing, fragment anchors and dev-build comments change
textContentand the raw server string, never layout — a test asserting exact text must.trim(). Breadcrumb's ellipsis glyph is padded with spaces; Stepper's bullet reads "3not started" where Svelte reads "3 not started"; DatePicker's trigger loses the space before its calendar emoji; BentoGridCard's CTA rendersLearn more →tight; ToolCall's "Request" label and "Nothing recorded yet." placeholder gain surrounding spaces. Pagination's slot fallbacks carry<!--[-->…<!--]-->anchors in the server string, and its "hidden pages" comment and InlineCitation's template comments appear in dev builds. BlurReveal emitsclass=""when given no class; LiquidText omits an emptyfont-familyrather than emittingfont-family:;; Combobox's hidden submission input bindsvaluewith.attr, so acloneNode()of it carries the value too. - types: folder barrels export the
<Name>Propstype (the tooling contract) even where the Svelteindex.tsexports only the component:ContainerScrollProps,DirectionAwareHoverProps,FluidCursorProps(also asFluidCursorAdvancedPropsfor the deprecatedFluidCursorAdvancedalias),LiquidGlassProps,MarqueePropsandReviewCardProps; StatusMorph also exportsStatusMorphState.AppleCardPropsis exported fromAppleCard.vuebut, as in Svelte, not from the folder'sindex.ts.DropdownMenuSubPropsis an empty interface, kept for the contract. - fallbacks: where the Svelte source throws on a missing browser API, the Vue port degrades quietly: BoxReveal reveals immediately with no
IntersectionObserver; DisplacementText swallows three'sError creating WebGL contextand leaves the labelled host (role="img",aria-label) empty; EditorialEngine settles immediately with nodocument.fonts; FireworksHdr'sdebugisfalseoutside a bundler (import.meta.env?.DEV === true). Missing providers are under context keys. - kept source behaviour: Svelte behaviour is ported as-is, known quirks included, where the React package differs: ApprovalCard's
onApprove/onDenystay() => void(React widened them to(state) => void); Compare'sStarFieldbuilds its sky once, ignoring laterstarsCount/seedchanges; EditorialEngine trackspullquotesby array identity (hoist an inline array to keep it stable); LogoCloud's cloned marquee tracks carry noaria-hidden; MatrixRain does not pause off-screen; ContainerTextFlip has no stale-interval guard; AnimatedTestimonials has no reduced-motion handling; FlipWords goes blank ifwordsempties mid-exit ((index + 1) % 0storesNaN, plus a dev-only[Vue warn]: VNode created with invalid key (NaN)); Autocomplete and NavigationMenu publish the requesteddata-align="start"whiletransform-originfollows the resolved alignment;DropdownMenuContentkeeps the source'snextTick()wait before moving the panel to its edge. - composables: composables expose no
destroy(); cleanup runs throughonScopeDispose. Focus-trap composables return a handle (returnFocusNow/rearm) instead of imperative action methods. Media-query and similar composables keep an explicitstart/stoppair and accept getter-shaped options, as the Svelte actions do. - hydration: sound preferences hydrate explicitly in
onMounted(hydrateSound()) rather than readinglocalStorageduring setup, because Vue's production hydration does not patch an attribute mismatch introduced by an eagerly read client-only preference. - vue floor: Vue 3.5 is the floor, not just the minimum peer range: the port relies on reactive props destructuring,
useTemplateRef,onWatcherCleanupand<Teleport defer>. - upstream fixes: a handful of proven bugs surfaced while porting
fancy-ui-reactwere fixed on the Svelte source itself, and the Vue port inherits those fixes. The review of the Vue port surfaced more, fixed here first with Svelte parity pending (each listed under its component below): animated-testimonials, animated-tooltip, autocomplete, compare, confetti, copy-button, focus, inline-citation, number-input, rainbow-button, select, text-generate-effect, tooltip. - not listed: implementation-only differences with no consumer surface (template duplication, style-object plumbing, internal helper types, test-fixture transpositions) are not recorded here. A component without a bullet below differs from Svelte only through the rules above.
Per component
- animated-beam:
containerRef,fromRefandtoRefare typedHTMLElement | nullrather than a requiredHTMLElement: a Vue template ref isnulluntil the parent has mounted, so the idiomatic:container-ref="container"binding passesnullon the first render. They are still required props (passnull, not nothing). - animated-beam: Ids come from
useFancyId()and readbeam-v-N, with the-glow/-bloomfilter suffixes. Svelte usesbeam-<$props.id()>. Vue'suseId()is unique within one app, not across separately mounted apps, so the uniqueness test mounts two beams in one app. - animated-beam: The ResizeObserver arms on
onMountedand again from aflush: 'post'watcher oncontainerRef. A parent's template ref is stillnullwhen a child'sonMountedruns, a gap Svelte'sbind:thisdoes not have. The watcher also re-arms the observer when the container is swapped. - animated-testimonials: the active testimonial is clamped when the list shrinks, and an empty list during a transition resets the index to 0 instead of
NaN(upstream fix, Svelte parity pending). - animated-tooltip: tooltip ids are generated per avatar (
useFancyId()) instead ofanimated-tooltip-${item.id}, soaria-describedbystays a single token and stays unique across rows that share item ids (upstream fix, Svelte parity pending). - apple-card-carousel:
AppleCardData.contentis a Vue component or render function (Componentfromvue, rendered through<component :is>) where Svelte takes aSnippet. It cannot become a slot because it lives inside an item of thecardsarray: passcontent: () => h('div', …)or a small component. - autocomplete: when the filtered list changes while open, the active option follows the highlighted suggestion by its text and clears when that suggestion is gone, so
aria-activedescendantnever points at a missing option (upstream fix, Svelte parity pending). - bento-grid: the source's
:global(.dark) .bento-tile …dark-theme rules are written.dark .bento-tile …in the scoped block, because the scoped compiler would reduce a selector containing:global(...)to a bare.dark. The rule targets the same elements with the same specificity. - bento-grid: tile content parts (
.bento-lift,.bento-icon,.bento-cta) are reached with:deep()from the internal BentoFrame, and the grid's reveal rules use> :deep(*), in place of the source's:global(). The compiled selectors are unchanged apart from the scope attribute. - bento-grid: the child indexing (
--bento-i,--bento-delay, re-run by a MutationObserver) and the one-shot in-view observer run fromflush: 'post'watchers on the grid's template ref, and reduced motion comes fromuseReducedMotion(), which is read inonMounted. The SSR output is the same as the source's: the classbento-revealwithdata-state="armed", and the tiles have no inline index properties until the watchers run on the client. - bento-grid: BentoGridItem and BentoGridCard pass
[base, class]to the internal frame'scn()where the source uses[base, className].join(" "). The merged string is identical for string classes, and the port also accepts Vue's array and object class values. - border-beam: The dark-theme rule
:global(.dark) .border-beamis written as.dark .border-beamunder the scoped compiler, which only scopes.border-beamand gives the same selector; a leading:global()would instead produce a bare.darkrule. - border-beam: The
bb-travelandbb-ignitekeyframes stay in the scoped block. They are only referenced from scopedanimationdeclarations, so the compiler renames them with the scope hash and rewrites those references consistently. This means an outside stylesheet cannot target the keyframes by their bare names. - compare: autoplay keeps a single frame loop:
startAutoplaystops the running loop first, so mount, hover leave, drag end and prop restarts no longer stack loops, and unmount cancels it (upstream fix, Svelte parity pending). - composer:
ComposerCommandMenuattaches its textareakeydownlistener in the CAPTURE phase (in Vue every listener sits on the textarea itself, so a bubble-phasestopPropagation()would arrive too late to stop Enter-to-send). A consumer's ownkeydownlistener on the textarea therefore runs after the menu's while it is open. - compare: the callbacks are the camelCase props
onPercentageChange,onDragStart,onDragEnd,onHoverEnter,onHoverLeave, so@percentage-change,@drag-start,@drag-end,@hover-enterand@hover-leavework. The lowercaseonpercentagechange,ondragstart,ondragend,onhoverenter,onhoverleavestay as deprecated aliases (:ondragstart="fn"), called only when the camelCase prop is absent; the run-together@dragstart/@hoverenterspellings compile toonDragstart/onHoverenterand are dropped with the other attributes. - confetti: Reassigning
optionsand callingfire()in the same tick bursts with the PREVIOUS object: Vue hands a reassigned prop to the child only on the parent's next render, where Svelte bursts with the new one. Mutating the object already passed asoptionsbehaves identically, and the nextfire()after the re-render uses the reassignment. - confetti:
ConfettiButtonfollows the same contract as Svelte:typedefaults to"button"and is overridable throughattrs,classis merged, native attributes are forwarded, andonClickis a declared prop (arrays accepted) called once after the burst. - container-scroll: the source's
:global(.dark) .cs-rootand:global(html:not(.dark)) .cs-rootselectors are written as plain ancestor selectors (.dark .cs-root,html:not(.dark) .cs-root) in the scoped style block, because the scoped compiler would otherwise emit the wrapped part alone; the matched elements and the values are identical. - context-menu:
openAtdrops the source's imperativeanchorRef.style.setProperty("left"/"top")writes and relies on the anchor span's:stylebinding landing ahead of theflush: "post"recompute. A consumer readingctx.anchorRef.style.leftsynchronously insideonOpenChangesees the PREVIOUS coordinates in Vue and the new ones in Svelte. - copy-button:
onCopyreports the value captured when the copy started, not the prop's later value, when the prop changes while the clipboard write is pending (upstream fix, Svelte parity pending). - datamosh-transition:
phaseis output-only:v-model:phasehears every change, but a value the parent writes back is ignored, where Svelte's bindable adopts it.@covered/@revealed(theonCovered/onRevealedprops) are the callbacks; the lowercaseoncovered/onrevealedstay as deprecated aliases, called only when the camelCase prop is absent.cover()/reveal()/play()are called through a template ref. - dim-siblings: Svelte's child
:global(...)selector becomes:slotted(...): only direct children that arrived through the default slot dim. DOM injected into the root from outside the slot does not participate, where Svelte's rule would still catch it. - direction-aware-hover: The two
:global()rules keep their declarations verbatim but lose their document-wide reach: the.group\/card44px min-box and theprefers-reduced-motion: reducetransition-duration: 0.1soverride apply to this component's own elements plus its whole slot subtree, because every scoped style ships in the onestyles.csseach consumer imports once, and would otherwise force both on apps that never render the component. Under reduced motion the 0.1s override therefore reaches this component's slot content only, not the rest of the host page. Matches the React package's identical narrowing. - dock: Svelte throws when a DockIcon or DockSeparator is mounted outside a Dock. The Vue port degrades instead, through a frozen fallback context:
magnify: false,orientation: "horizontal", the pointer atInfinityandreflection: true(the prop default). A lone icon renders at its resting 40px with the reflection hook, and a lone separator is a verticalrole=separatorhairline. - dock: Svelte writes
:global(.dark) .dock-*in its scoped styles. Vue writes them as plain.dark .dock-*ancestor selectors, which have the same reach and the same specificity. - flip-card:
inertis bound ascond || undefinedrather than a bare boolean: when Vue patches an element with noinertDOM property (jsdom) it writesinert="false", which would make the visible face inert, so false is mapped to no attribute; browser behaviour is unchanged. - flip-card: Reduced motion is read through
useReducedMotion(), which starts the media query in onMounted, instead of Svelte's$effect(() => reduced.start()), following the package's rule that SSR and hydration must render the same thing. - focus: toggling
manualModeafter mount starts or stops the auto-cycle, and switching to manual returns focus to the first word (upstream fix, Svelte parity pending). - inline-citation: a source url or
hrefwith no scheme (docs.example.dev/guide) getshttps://added before the scheme check, the same asSourceCard, so the marker no longer resolves against the app origin (upstream fix, Svelte parity pending). - input:
autocompleteis widened tostringin the props the component accepts; the exportedInputPropsstill types it as the full autofill token union. The generated props type would otherwise exceed the type checker's union ceiling, so a token typo is no longer caught at the call site. - line-hover-link:
aria-labelis spelledariaLabelinLineHoverLinkProps: Vue camelises declared prop names, so the hyphenated Svelte key could never be read back offprops. BothariaLabel="…"andaria-label="…"reach the prop and emitaria-labelon the anchor. - line-reveal: Line height survives a font-only change. Svelte's
style:font/style:line-heightdirectives re-apply only the declaration whose value changed, so switching weight or family at the same size re-applies thefontshorthand (which resetsline-height) without the longhand, and the lines drift inside their fixed-height masks. Vue re-applies every key of the style object,fontthenline-height, on each patch, so that bug does not reproduce; the emitted style still readsfont: …; line-height: …. Behavioural parity with the React port. - marquee: the Web Animations upgrade is rebuilt when
classchanges, as in Svelte, but becauseclassis typedHTMLAttributes["class"]here, a consumer passing an inline array or object class gets a new identity on every parent render and triggers a rebuild each time; the rebuild resumes from the current position, so nothing jumps visually. Pass a string, or hoist the array, to avoid the churn. - marquee: ReviewCard's dark-mode rule is written
.dark .review-cardinstead of the source's:global(.dark) .review-card, because the scoped compiler would swallow the whole selector inside:global(); the emitted selector has the same reach (.darkancestor, scoped card). - navigation-menu:
NavigationMenuContextgains one readonly member the source has no counterpart for,pendingFocusValue: a Vue watcher's callback is untracked, so the panel names the pending focus request as an explicit watch source (consumeFocusRequest()is still the only way to take it). Without it, Enter on a trigger whose panel is already hover-open would not move focus into the panel. - number-input: step precision counts the exponent of steps written in exponential notation (
1e-7), so tiny fractional steps no longer round back to an integer (upstream fix, Svelte parity pending). - pagination: when the visible run of pages shifts, the numbers glide with the list's CSS move transition (300ms, cubic-bezier(0.16, 1, 0.3, 1), off under reduced motion) instead of a JS-sampled expo-out FLIP; the sliding current-page pill is identical.
- password-input: Cross-fade exit easing differs from the source, package-wide and shared with the React port: the icon cross-fade's EXIT leg resolves to
JS_EASINGS.in, because the presence core passes a real per-leg direction, where the source's bidirectionaltransition:iconFadereports direction "both" and eases both legs withJS_EASINGS.out. - pressable:
PressablePropsno longer blocks the seven listeners the wrapper owns (onpointerdown,onpointerup,onpointercancel,onpointerleave,onfocusout,onkeydown,onkeyup): passed through$attrs, a same-named handler is MERGED and fires alongside the wrapper's own (caller's first) rather than being silently overridden. Put your own handlers for those on the child; the guidance is advisory here, not type-enforced. - pulse-beam: the callbacks are the camelCase props
onFadeIn/onFadeOut(@fade-in/@fade-out); the lowercaseonfadein/onfadeoutstay as deprecated aliases, called only when the camelCase prop is absent. The computed--pb-*custom properties win over a consumerstyle. - radio-group: The generated
namereadsv-0-name,v-1-name, … from Vue'suseId(), whose counter is per APP, not per page: two RadioGroups mounted in two SEPARATE Vue apps on one page both getv-0-nameand fight over each other's selection. Inside one app they are distinct and SSR-stable. Consumers mounting several apps must setapp.config.idPrefixper app, or pass an explicitname; the Svelte and React packages have no equivalent hazard. - rainbow-button: a disabled
hrefanchor callspreventDefault()on click, so it no longer navigates; the Svelte source only returns early, andaria-disabled/tabindex=-1do not stop a pointer click from following the href (upstream fix, Svelte parity pending). - reveal: Changing
once,thresholdorrootMarginno longer tears down and rebuilds the observer:useInViewroutes them through the core'supdate(), and a spentonce: trueobserver does not re-fire (same as the React port). - select: a disabled Select rejects every commit, and closes its panel if it is disabled while open, so a portalled row click can no longer change the value or fire
onValueChangeon a disabled control (upstream fix, Svelte parity pending). - smooth-cursor: The custom cursor is the
#cursorslot rather than a snippet prop. The tip-versus-centre hotspot is recomputed from the slot's presence on every render, not held in a reactive derived value, because slot presence is not reactive in Vue. - smooth-cursor: The gradient id for the default arrow comes from
useFancyId()(`smooth-cursor-fill-v-
Related Packages
Accessible Vue UI components built on Ark UI and styled with Tailwind CSS v4. 80...
main theme file for manguito components library. Other MCL components will need...
Vibe UI — a Vue 3 component library for vibe coders: 50+ components, dark/light...
Version History
| Version | Released | Vue | License |
|---|---|---|---|
| 0.4.0 | ^3.5.2 | MIT | |
| 0.3.0 | ^3.5.2 | MIT | |
| 0.2.0 | ^3.5.2 | MIT | |
| 0.1.1 | ^3.5.2 | MIT | |
| 0.1.0 | ^3.5.2 | MIT | |
| 0.0.0 | ^3.5.2 | MIT |
Pre-releases (1)
| Version | Released | License |
|---|---|---|
| 0.0.0-stage |