@veryrogue/winui-vue
v0.0.49
Published
Vue 3 components built on Fluent UI v3 web components — composed helpers (NavRail, ScrollArea, Carousel, Popover, TagPicker, Toast, Date/Time pickers) plus a Tailwind v4 token alias system.
Downloads
3,826
Maintainers
Readme
@veryrogue/winui-vue
Vue 3 components built on top of Fluent UI v3 web components. Ships:
- Composed helpers that don't exist as
fluent-*web components yet —NavRail,SideNav(with mobile drawer),Select(single + multi, searchable, grouped),ScrollArea,Carousel,Popover,TagPicker,Toast,Tabs,AccordionItem,Calendar,Card,DatePicker,TimePicker,SearchBox,RouterView(page transitions). - Compound APIs for the bigger ones (shadcn-style children) —
Carousel/CarouselContent/CarouselItem/CarouselNext/CarouselPrevious/CarouselDots,Popover/PopoverTrigger/PopoverContent/PopoverClose,TagPicker/TagPickerInput/TagPickerList/TagPickerOption,NavRail/NavRailItem. - Tailwind v4 token aliases —
bg-bg-1,text-fg-2,border-stroke-2,shadow-fl-4,rounded-fl-md,p-fl-m,text-fl-300,font-fl-semibold, …
Install
bash / zsh / Git Bash — backslash line continuations:
npm install @veryrogue/winui-vue \
@fluentui/web-components @fluentui/tokens @microsoft/fast-element \
vue tailwindcss @tailwindcss/vitePowerShell — single line (PowerShell can't continue lines with \; use a backtick ` if you really want to wrap):
npm install @veryrogue/winui-vue @fluentui/web-components @fluentui/tokens @microsoft/fast-element vue tailwindcss @tailwindcss/vitecmd.exe — same as PowerShell, single line:
npm install @veryrogue/winui-vue @fluentui/web-components @fluentui/tokens @microsoft/fast-element vue tailwindcss @tailwindcss/viteSetup
vite.config.ts — register Fluent custom elements with Vue:
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import tailwindcss from '@tailwindcss/vite'
export default defineConfig({
plugins: [
tailwindcss(),
vue({
template: {
compilerOptions: {
isCustomElement: (tag) => tag.startsWith('fluent-'),
},
},
}),
],
})src/style.css — your Tailwind entry. Import the lib's @theme aliases and tell Tailwind to scan the lib's compiled output so utilities used inside the library (flex, bg-bg-1, rounded-fl-md, …) make it into your CSS:
@import "tailwindcss";
@import "@veryrogue/winui-vue/theme.css";
@source "../node_modules/@veryrogue/winui-vue/dist";Why both
@importand@source?theme.cssdefines the design-token aliases (so utilities likebg-bg-1exist).@sourcetells your Tailwind to scan the library's bundle for class strings used in component templates. Combined, you get one Tailwind output covering both your code and the library — no duplicated preflight, no doubled utilities.
src/main.ts — register the Fluent components you'll use, set the theme, import both the lib's runtime CSS and your Tailwind entry:
import { createApp } from 'vue'
import App from './App.vue'
import '@fluentui/web-components/button/define.js'
import '@fluentui/web-components/badge/define.js'
// …import the rest you need from `@fluentui/web-components/<name>/define.js`
import { setTheme } from '@fluentui/web-components'
import { webDarkTheme } from '@fluentui/tokens'
setTheme(webDarkTheme)
import '@veryrogue/winui-vue/style.css' // lib runtime: Fluent host preflight + .fl-* component CSS
import './style.css' // your Tailwind entry (above)
createApp(App).mount('#app')Usage
<script setup lang="ts">
import { ref } from 'vue'
import {
NavRail, NavRailItem,
Carousel, CarouselContent, CarouselItem,
CarouselPrevious, CarouselNext, CarouselDots,
FlToast, // ← required if you want toasts to actually render
useToasts,
} from '@veryrogue/winui-vue'
const { toasts, push, dismiss } = useToasts()
const active = ref('home')
</script>
<template>
<div class="bg-bg-1 text-fg-1 p-fl-l rounded-fl-md shadow-fl-4">
<NavRail v-model="active">
<NavRailItem id="home" label="Home" />
<NavRailItem id="settings" label="Settings" />
</NavRail>
<Carousel :autoplay="3000">
<CarouselContent>
<CarouselItem>Slide 1</CarouselItem>
<CarouselItem>Slide 2</CarouselItem>
<CarouselItem>Slide 3</CarouselItem>
</CarouselContent>
<CarouselPrevious />
<CarouselNext />
<CarouselDots />
</Carousel>
<fluent-button @click="push('success', 'It works!')">Show toast</fluent-button>
</div>
<!-- Toast portal — REQUIRED for `push()` to actually render anything.
Mount this once at the root of your app (typically in App.vue). -->
<div class="fixed top-4 right-4 flex flex-col gap-2 z-[9999] pointer-events-none">
<FlToast
v-for="t in toasts"
:key="t.id"
:toast="t"
@dismiss="dismiss(t.id)"
/>
</div>
</template>Components reference
Important — import paths. Everything from this library is imported by name from the package, never from internal
@/...paths:// ✅ Correct import { FlNavRail, useToasts } from '@veryrogue/winui-vue' // ❌ Wrong — that's an internal path inside the library's source. // Won't resolve in your project. import FlNavRail from '@/components/FlNavRail.vue'Every component below is a named export — wrap it in braces, no default imports.
One import block with everything
Copy this into a file, then delete the lines for things you don't use. Tree-shaking removes the unused ones from your bundle.
import {
// ── Composed widgets ─────────────────────────────────────
FlAccordionItem,
FlCalendar,
FlCard,
FlCarousel,
FlDatePicker,
FlNavRail,
FlPopout,
FlRouterView,
FlScrollArea,
FlSelect,
FlSideNav,
FlSearchBox,
FlTabs,
FlTagPicker,
FlTimePicker,
FlToast,
// ── Compound APIs (use the parts together) ───────────────
Carousel, CarouselContent, CarouselItem,
CarouselNext, CarouselPrevious, CarouselDots,
Popover, PopoverTrigger, PopoverContent, PopoverClose,
TagPicker, TagPickerInput, TagPickerList, TagPickerOption,
NavRail, NavRailItem,
// ── Composable ───────────────────────────────────────────
useToasts,
} from '@veryrogue/winui-vue'
// One-time CSS import (in your main.ts is fine)
import '@veryrogue/winui-vue/style.css'
// Type-only imports — only if you're writing TS
import type {
FlNavItem, FlTab, TagOption,
Placement, TriggerKind,
Toast, ToastIntent, ToastAction, ToastOptions,
} from '@veryrogue/winui-vue'Per-component cheat sheet
Each entry shows: import → what it is → minimal example.
Each snippet below is fully self-contained — copy the entire
.vueblock, paste into a component, and it works. The imports are inside<script setup>so you don't need to read separately.Input sizing.
FlSearchBox,FlDatePicker, andFlTimePickerdefault towidth: 240px; max-width: 100%. To make one fill its parent, wrap it:<div class="w-full"><FlSearchBox /></div>or<div class="max-w-[480px]"><FlSearchBox /></div>.Custom icons. Same three components plus
NavRailItemaccept an#iconslot — drop in an emoji, an inline<svg>, an icon-component, anything. Skip the slot to get the default glyph.
FlAccordionItem
Animated disclosure section. Pass :animated="false" for an instant open/close (no height tween).
<script setup lang="ts">
import { FlAccordionItem } from '@veryrogue/winui-vue'
</script>
<template>
<FlAccordionItem heading="What is this?" :expanded="true">
Anything here. Each item is independent.
</FlAccordionItem>
<FlAccordionItem heading="Locked" :disabled="true">…</FlAccordionItem>
</template>FlCalendar
Standalone month grid. Cells outside the current month are dimmed; today is brand-tinted; selection is brand-filled.
<script setup lang="ts">
import { ref } from 'vue'
import { FlCalendar } from '@veryrogue/winui-vue'
const date = ref(new Date())
</script>
<template>
<FlCalendar v-model="date" :first-day-of-week="1" />
</template>FlCard
Surface-style container with anatomy: [media] → [header (title/description + headerAction)] → [content] → [footer + actions]. Every region is optional.
Props:
appearance—'filled'(default)| 'filled-alternative' | 'outline' | 'subtle'size—'small' | 'medium'(default)| 'large'— controls padding + font scaleorientation—'vertical'(default)| 'horizontal'shadow—'none' | 'sm'(default)| 'md' | 'lg' | 'xl'interactive— boolean. Auto-enabled whenhref,to, oras="button"is setselected— boolean. Toggles a brand-colored ring; emitsselecton clickdisabled— booleantitle,description— convenience text props (or use#title/#descriptionslots)media,mediaAlt— convenience image URL (or use#mediaslot for video/gradient/etc.)mediaPosition—'top'(default)| 'bottom' | 'left' | 'right'mediaAspect— CSS aspect ratio for the media region (e.g.'16/9','1/1')mediaSize— fixed media width/height in px (or any CSS length) forleft/righthref— renders as<a>;to— renders as<router-link>;as— force tag ('div' | 'a' | 'button' | 'article' | 'section' | 'li')
Slots: default (main content), title, description, media, headerAction (e.g. menu button), footer, actions (button row, right-aligned in footer).
Events: click, select (only when selected prop is bound).
Minimal example — convenience props:
<script setup lang="ts">
import { FlCard } from '@veryrogue/winui-vue'
</script>
<template>
<FlCard
title="Quarterly report"
description="Performance metrics for Q4 2025"
media="https://picsum.photos/600/300"
media-aspect="16/9"
/>
</template>Rich example — every region used:
<script setup lang="ts">
import { FlCard } from '@veryrogue/winui-vue'
import '@fluentui/web-components/button/define.js'
function open() { console.log('opening…') }
</script>
<template>
<FlCard
appearance="filled"
size="medium"
shadow="md"
interactive
media="https://picsum.photos/600/240"
media-aspect="16/9"
title="Project Atlas"
description="Last edited 2 hours ago"
@click="open"
>
<template #headerAction>
<fluent-button appearance="subtle" icon-only aria-label="More">⋯</fluent-button>
</template>
<p>A short blurb about the project lives here in the default slot.</p>
<template #footer>3 collaborators</template>
<template #actions>
<fluent-button appearance="subtle">Share</fluent-button>
<fluent-button appearance="primary">Open</fluent-button>
</template>
</FlCard>
</template>Selectable card (acts like a checkbox):
<script setup lang="ts">
import { ref } from 'vue'
import { FlCard } from '@veryrogue/winui-vue'
const picked = ref(false)
</script>
<template>
<FlCard
interactive
:selected="picked"
@select="picked = $event"
title="Premium plan"
description="$12 / month"
/>
</template>Horizontal card (image on the left):
<FlCard
orientation="horizontal"
media-position="left"
:media-size="120"
media="https://picsum.photos/240/240"
title="Album title"
description="Artist · 2024"
/>As a router link:
<FlCard :to="{ name: 'About' }" title="About" description="Read more →" />FlCarousel
All-in-one slide deck. Either pass a :slides array OR a :count and use the #slide slot. controls is 'overlay' | 'inline' | 'none'.
<script setup lang="ts">
import { FlCarousel } from '@veryrogue/winui-vue'
</script>
<template>
<FlCarousel :slides="['A', 'B', 'C']" :autoplay="3000" :height="160">
<template #slide="{ item, index }">
<div class="flex items-center justify-center h-full bg-pal-blue">
{{ item }} ({{ index + 1 }})
</div>
</template>
</FlCarousel>
</template>FlDatePicker
Input field + popover-anchored FlCalendar. Forwards firstDayOfWeek, minDate, maxDate. Defaults to 240 px wide; wrap in a sized container (<div class="max-w-[400px]">…) or w-full parent to override. Use #icon to swap the calendar glyph.
<script setup lang="ts">
import { ref } from 'vue'
import { FlDatePicker } from '@veryrogue/winui-vue'
const date = ref<Date | null>(null)
</script>
<template>
<FlDatePicker v-model="date" placeholder="Pick a date…" />
<!-- Custom icon: anything works in the slot — emoji, inline SVG, icon component… -->
<FlDatePicker v-model="date">
<template #icon>📅</template>
</FlDatePicker>
<!-- Full-width inside a constrained parent -->
<div class="max-w-[400px]">
<FlDatePicker v-model="date" />
</div>
</template>FlNavRail
Vertical nav with a sliding brand-color indicator. Pass :items as FlNavItem[]. Use the #icon scoped slot for custom icon rendering, or include an icon SVG string per item.
<script setup lang="ts">
import { ref } from 'vue'
import { FlNavRail, type FlNavItem } from '@veryrogue/winui-vue'
const items: FlNavItem[] = [
{ id: 'home', label: 'Home' },
{ id: 'settings', label: 'Settings' },
{ id: 'archive', label: 'Archived', disabled: true },
]
const active = ref('home')
</script>
<template>
<FlNavRail v-model="active" :items="items" />
</template>FlPopout
Floating panel anchored to a trigger. placement of above|below|before|after. Use :stay-open="true" for click-to-toggle modal mode (with a × close button), or trigger="hover" for tooltip behavior.
<script setup lang="ts">
import { FlPopout } from '@veryrogue/winui-vue'
</script>
<template>
<FlPopout placement="below" trigger="hover" content="Tooltip text">
<template #trigger>
<fluent-button>Hover me</fluent-button>
</template>
</FlPopout>
</template>FlRouterView
Drop-in replacement for <router-view> with built-in page transitions. Direction is inferred from route depth (deeper = forward, shallower = backward), so /users → /users/42 slides forward and back slides backward — no config needed.
Props: transition ('slide' | 'fade' | 'none', default 'slide'), axis ('horizontal' | 'vertical', default 'horizontal'), mode (Vue <Transition> mode), keyBy ('path' | 'fullPath', default 'fullPath' — re-mounts on query changes).
Per-route override via route meta:
// router/index.ts
import { createRouter, createWebHistory } from 'vue-router'
export default createRouter({
history: createWebHistory(),
routes: [
{ path: '/', component: () => import('@/views/Home.vue'), meta: { transition: 'fade' } },
{ path: '/about', component: () => import('@/views/About.vue'), meta: { transition: 'none' } },
{ path: '/docs', component: () => import('@/views/Docs.vue'), meta: { axis: 'vertical' } },
],
})<!-- App.vue -->
<script setup lang="ts">
import { FlRouterView } from '@veryrogue/winui-vue'
</script>
<template>
<main class="relative">
<FlRouterView />
<!-- Or pick a global default:
<FlRouterView transition="fade" />
<FlRouterView transition="slide" axis="vertical" /> -->
</main>
</template>Tip. During slide transitions both pages exist in the DOM at once (the leaving one is
position: absoluteand stacked on top, fading out fast). The component wraps<router-view>in its ownposition: relative; overflow: hiddenstage, so you don't need to add a wrapper. Pages get a default--colorNeutralBackground1background to prevent see-through; if your page root sets its own background, it wins.
Requires vue-router as a peer dependency in your project (npm i vue-router).
FlScrollArea
WinUI 11 / Win 11 Settings-style overlay scrollbar. Hides the native scrollbar and renders a thin pill that fattens on hover/scroll. Optional classic arrows.
<script setup lang="ts">
import { FlScrollArea } from '@veryrogue/winui-vue'
</script>
<template>
<div class="h-[300px]">
<FlScrollArea arrows :step-size="60">
<div class="p-4">…tall content…</div>
</FlScrollArea>
</div>
</template>FlSideNav
Sidebar navigation with sections, badges, vue-router integration, and a built-in mobile drawer. Below mobileBreakpoint (default 768 px) the nav becomes a slide-in drawer with a fixed hamburger toggle and a backdrop. Active state is auto-tracked via route.path when items use to.
Props:
items—FlSideNavItem[](single section) orFlSideNavSection[](grouped with section titles)title— brand text in the header (or use#headerslot)appearance—'filled'(default)| 'subtle' | 'transparent'width— px or CSS value when expanded (default260)collapsedWidth— px or CSS value in rail mode (default56)collapsed— boolean. Desktop-only icon railmobileBreakpoint— px viewport below which it switches to drawer (default768)closeOnNavigate— auto-close drawer on route change (defaulttrue)open— externally control drawer state (v-model:open)modelValue— active item id (only needed for non-router apps)toggleClass— class for the mobile hamburger button (override its position)animatedIndicator— boolean (defaulttrue). NavRail-style sliding indicator bar that smoothly glides between active items. Setfalsefor no indicatorindicatorHeight— px height of the sliding bar (default16)density—'compact' | 'comfortable'(default)| 'spacious'. Vertical rhythm presetitemGap— px or CSS length. Override the gap between rowsitemPaddingY— px or CSS length. Override row vertical paddingindicatorColor— any CSS color ('#3b82f6','oklch(70% 0.2 240)','var(--my-token)'). Recolors the sliding barindicatorWidth— px or CSS length. Width of the sliding bar (default3px)
Per-pixel spacing. For full control, set the CSS custom properties on the
<FlSideNav>element:<FlSideNav class="my-nav" :items="items" /> <style> .my-nav { --fl-sidenav-item-gap: 8px; --fl-sidenav-item-pad-y: 10px; --fl-sidenav-item-row-min: 40px; --fl-sidenav-indicator-color: #f97316; --fl-sidenav-indicator-width: 4px; } </style>
Two things people call "rail" — pick the one you want:
| Feature | Prop | Default | What it does | |---|---|---|---| | Sliding indicator bar (the blue vertical line that animates between active items) |
animatedIndicator|true(on) | Visual cue showing active item | | Collapsed rail mode (icons only, ~56 px wide, Outlook-style strip) |collapsed|false| Hides labels & section headings, shrinks width tocollapsedWidth|They compose: a collapsed rail still gets the sliding indicator.
Collapsed rail (icon-only) — toggleable:
<script setup lang="ts">
import { ref } from 'vue'
import { FlSideNav } from '@veryrogue/winui-vue'
const items = [
{ id: 'home', label: 'Home', icon: '🏠', to: '/' },
{ id: 'about', label: 'About', icon: '📘', to: '/about' },
]
const railed = ref(false)
</script>
<template>
<button @click="railed = !railed">Toggle rail</button>
<FlSideNav :items="items" :collapsed="railed" />
</template>Item shape (FlSideNavItem):
{
id: string
label: string
icon?: string // emoji or raw SVG (v-html)
to?: RouteLocationRaw // vue-router target
href?: string // external link
target?: string // '_blank' etc.
badge?: string | number // chip on the right
disabled?: boolean
class?: any // extra classes merged onto the rendered link/button
style?: any // inline styles merged in
as?: string // override tag for this item only
meta?: Record<string, unknown> // free-form data for slot consumers
}Slots (all scoped where useful):
header,headerAction,search,footer,toggleIcon— chromeicon—{ item, active }— replace just the leading iconlabel—{ item, active }— replace just the label textbadge—{ item, active }— replace just the trailing chipitem—{ item, active, collapsed, props }— full escape hatch. Render anything;propsincludes the boundref,class, andonClickso the indicator still tracks your custom row- default — extra content below the list
Events: select(item), update:modelValue(id), update:open(value).
Sectioned router-driven nav (matches the photo):
<script setup lang="ts">
import { FlSideNav, type FlSideNavSection } from '@veryrogue/winui-vue'
const sections: FlSideNavSection[] = [
{
title: 'Primitives',
items: [
{ id: 'buttons', label: 'Buttons', icon: '🔘', to: '/buttons' },
{ id: 'typography', label: 'Typography', icon: '🔤', to: '/typography' },
{ id: 'badges', label: 'Badges', icon: '🏷️', to: '/badges', badge: 'New' },
{ id: 'avatar', label: 'Avatar & Image', icon: '👤', to: '/avatar' },
{ id: 'inputs', label: 'Inputs', icon: '⌨️', to: '/inputs' },
],
},
{
title: 'Composed',
items: [
{ id: 'card', label: 'Card', icon: '🟦', to: '/card' },
{ id: 'sidenav', label: 'Side Nav', icon: '📚', to: '/sidenav', badge: 3 },
],
},
]
</script>
<template>
<div class="flex min-h-dvh">
<FlSideNav title="Fluent UI v3 · Gallery" :items="sections" />
<main class="flex-1 p-fl-l">
<router-view />
</main>
</div>
</template>With a search box and footer:
<script setup lang="ts">
import { FlSideNav, FlSearchBox } from '@veryrogue/winui-vue'
const items = [
{ id: 'home', label: 'Home', icon: '🏠', to: '/' },
{ id: 'about', label: 'About', icon: '📘', to: '/about' },
{ id: 'settings', label: 'Settings', icon: '⚙️', to: '/settings', disabled: true },
]
</script>
<template>
<FlSideNav :items="items">
<template #search>
<FlSearchBox placeholder="Search…" />
</template>
<template #footer>
<small class="text-fg-3">v0.1.0</small>
</template>
</FlSideNav>
</template>Customizing per-item look (per-item class + #label slot):
<script setup lang="ts">
import { FlSideNav } from '@veryrogue/winui-vue'
const items = [
{ id: 'home', label: 'Home', icon: '🏠', to: '/' },
// Push this row to the bottom + give it a danger color via item.class.
{ id: 'logout', label: 'Sign out', icon: '🚪', href: '/logout', class: 'mt-auto !text-status-danger-fg' },
]
</script>
<template>
<FlSideNav :items="items">
<!-- Replace just the label rendering for every item -->
<template #label="{ item, active }">
<span :class="['fl-sidenav__label', { 'underline': active }]">
{{ item.label.toUpperCase() }}
</span>
</template>
</FlSideNav>
</template>Total custom item (the #item escape hatch):
When you use #item, you replace the default tag resolution — you decide whether to render <router-link>, <a>, <button>, or anything else. Always spread v-bind="props" so the indicator can find the row and clicks emit select / update:modelValue.
✅ Default — use <router-link> for routed items (works with SPA navigation):
<script setup lang="ts">
import { FlSideNav } from '@veryrogue/winui-vue'
const items = [
{ id: 'home', label: 'Home', icon: '🏠', to: '/', meta: { count: 3 } },
{ id: 'about', label: 'About', icon: '📘', to: '/about', meta: { count: 0 } },
]
</script>
<template>
<FlSideNav :items="items">
<template #item="{ item, active, props }">
<router-link v-bind="props" :to="item.to" class="my-custom-row">
<span class="text-2xl">{{ item.icon }}</span>
<span class="font-bold">{{ item.label }}</span>
<span v-if="item.meta?.count" class="ml-auto rounded-full bg-brand-bg px-2 text-xs">
{{ item.meta.count }}
</span>
<span v-if="active" class="text-brand-fg">●</span>
</router-link>
</template>
</FlSideNav>
</template>Alternative — render an <a> and push via the router on click:
Useful when you want anchor semantics (right-click → "Open in new tab" still works because of the real href) but SPA navigation on left-click. Use @click.prevent to stop the full page reload.
<script setup lang="ts">
import { useRouter } from 'vue-router'
import { FlSideNav } from '@veryrogue/winui-vue'
const router = useRouter()
const items = [
{ id: 'home', label: 'Home', icon: '🏠', to: '/' },
{ id: 'about', label: 'About', icon: '📘', to: '/about' },
]
</script>
<template>
<FlSideNav :items="items">
<template #item="{ item, active, props }">
<a
v-bind="props"
:href="item.to as string"
class="my-custom-row"
@click.prevent="router.push(item.to as any)"
>
<span class="text-2xl">{{ item.icon }}</span>
<span class="font-bold">{{ item.label }}</span>
<span v-if="active" class="text-brand-fg">●</span>
</a>
</template>
</FlSideNav>
</template>Pitfall. Plain
<a :href="item.to">without@click.preventcauses a full page reload in SPAs (the URL ends up like/about#/with hash routing). Use<router-link>or pair the<a>withrouter.pushas shown above.
Externally controlled drawer (open it from anywhere):
<script setup lang="ts">
import { ref } from 'vue'
import { FlSideNav } from '@veryrogue/winui-vue'
const open = ref(false)
</script>
<template>
<button @click="open = true">Menu</button>
<FlSideNav v-model:open="open" :items="items" />
</template>Tip — placement. On desktop the nav is a normal flex child (give it a fixed-width parent or let it size itself). On mobile it goes
position: fixedautomatically; the hamburger toggle floats top-left at12px / 12px. Override withtoggle-class="!top-4 !right-4 !left-auto"etc.
FlSelect
Single- or multi-select dropdown with optional search, grouping, custom option rendering, and full keyboard support. Built from scratch (no <fluent-select> dep) so the panel auto-flips above the trigger when there's no room below, and stays positioned via position: fixed even while the page scrolls.
Props:
modelValue—string(single) orstring[](multi). Use withv-modeloptions—FlSelectOption[]({ value, label, disabled?, group? }) or plainstring[]for label-only optionsmultiple— boolean. Toggle to select more than onesearchable— boolean. Adds a filter input at the top of the dropdownclearable— boolean. Shows a × in the trigger when there's a valueplaceholder— text shown when nothing is selected (default'Select…')searchPlaceholder— placeholder for the search input (default'Search…')emptyText— text shown when the filter matches nothing (default'No results')disabled,invalid— booleans for statesize—'small' | 'medium'(default)| 'large'appearance—'outline'(default)| 'filled' | 'underline'maxHeight— px or CSS length, max panel height (default280)filter—(opt, query) => boolean— custom filter function. Default: case-insensitive label match
Slots: prefix (icon before the value), selected ({ selected, empty } — replace the in-trigger display), option ({ option, selected } — custom row in the dropdown), empty (no-results state), footer (sticky bottom area inside the panel).
Events: update:modelValue, change, open, close, clear.
Exposed methods: open(), close(), toggle(), clear() — call via template ref.
Single — string options:
<script setup lang="ts">
import { ref } from 'vue'
import { FlSelect } from '@veryrogue/winui-vue'
const fruit = ref<string | null>(null)
</script>
<template>
<FlSelect v-model="fruit" :options="['Apple', 'Banana', 'Cherry']" placeholder="Pick a fruit" clearable />
</template>Multi — object options + groups + search:
<script setup lang="ts">
import { ref } from 'vue'
import { FlSelect, type FlSelectOption } from '@veryrogue/winui-vue'
const picked = ref<string[]>([])
const options: FlSelectOption[] = [
{ value: 'red', label: 'Red', group: 'Warm' },
{ value: 'orange', label: 'Orange', group: 'Warm' },
{ value: 'yellow', label: 'Yellow', group: 'Warm' },
{ value: 'green', label: 'Green', group: 'Cool' },
{ value: 'blue', label: 'Blue', group: 'Cool' },
{ value: 'purple', label: 'Purple', group: 'Cool', disabled: true },
]
</script>
<template>
<FlSelect
v-model="picked"
:options="options"
multiple
searchable
clearable
placeholder="Pick colors"
/>
</template>Custom option rendering (avatars, swatches, descriptions):
<template>
<FlSelect v-model="user" :options="users">
<template #option="{ option, selected }">
<img :src="(option as any).avatar" class="w-5 h-5 rounded-full" />
<span class="flex-1">{{ option.label }}</span>
<span v-if="selected" class="text-brand-fg-1">✓</span>
</template>
</FlSelect>
</template>Custom selected display (badge stack with avatars instead of chips):
<template>
<FlSelect v-model="picked" :options="users" multiple>
<template #selected="{ selected, empty }">
<span v-if="empty" class="fl-select__placeholder">Pick teammates</span>
<span v-else class="flex -space-x-1">
<img v-for="u in selected" :key="u.value" :src="(u as any).avatar"
class="w-5 h-5 rounded-full ring-2 ring-bg-1" />
</span>
</template>
</FlSelect>
</template>FlSearchBox
<fluent-text-input> with a leading magnifier icon + a × clear button. v-model + @search (Enter) + @clear events. Defaults to 240 px wide; wrap in a sized container or w-full parent to override. Use #icon to swap the magnifier glyph.
<script setup lang="ts">
import { ref } from 'vue'
import { FlSearchBox } from '@veryrogue/winui-vue'
const query = ref('')
function run(value: string) { console.log('searched for', value) }
</script>
<template>
<FlSearchBox v-model="query" placeholder="Search docs…" @search="run" />
<!-- Custom icon -->
<FlSearchBox v-model="query" placeholder="Filter…">
<template #icon>🔎</template>
</FlSearchBox>
<!-- Full-width inside a constrained parent -->
<div class="max-w-[480px]">
<FlSearchBox v-model="query" />
</div>
</template>FlTabs
Animated tabs. transition is 'slide' | 'fade' | 'none', axis is 'horizontal' | 'vertical'. Each tab has its own slot named after the tab id.
<script setup lang="ts">
import { ref } from 'vue'
import { FlTabs, type FlTab } from '@veryrogue/winui-vue'
const tabs: FlTab[] = [
{ id: 'overview', label: 'Overview' },
{ id: 'specs', label: 'Specs' },
]
const active = ref('overview')
</script>
<template>
<FlTabs v-model="active" :tabs="tabs" transition="slide">
<template #overview>Overview content</template>
<template #specs>Specs content</template>
</FlTabs>
</template>FlTagPicker
Multi-select chip input + filterable suggestion dropdown. v-model is string[] (selected option values).
<script setup lang="ts">
import { ref } from 'vue'
import { FlTagPicker, type TagOption } from '@veryrogue/winui-vue'
const opts: TagOption[] = [
{ value: 'ts', label: 'TypeScript' },
{ value: 'vue', label: 'Vue' },
]
const picked = ref<string[]>([])
</script>
<template>
<FlTagPicker v-model="picked" :options="opts" placeholder="Add…" />
</template>FlTimePicker
Time-slot picker. startTime / endTime / increment (minutes) / hourFormat ('12' or '24'). Defaults to 240 px wide; wrap in a sized container or w-full parent to override. Use #icon to swap the clock glyph.
<script setup lang="ts">
import { ref } from 'vue'
import { FlTimePicker } from '@veryrogue/winui-vue'
const time = ref('')
</script>
<template>
<FlTimePicker v-model="time" start-time="09:00" end-time="17:00" :increment="30" />
<!-- Custom icon -->
<FlTimePicker v-model="time">
<template #icon>⏰</template>
</FlTimePicker>
</template>FlToast + useToasts()
Toast notifications. useToasts() returns a global store; mount one <FlToast> portal in your root component, then push() from anywhere.
⚠ Two-step setup — don't skip step 1, or
push()will silently do nothing.
- Mount the toast portal once in your root component (usually
App.vue).- Anywhere in your app, call
push(...)to add toasts to the queue.The portal is what actually renders the toasts. Without it, calls to
push()succeed but you see nothing because there's no UI bound to the queue.
Step 1 — mount the portal in your root component:
<!-- App.vue -->
<template>
<RouterView />
<!-- ← Required. Always-mounted, fixed position, top of z-stack. -->
<div class="fixed top-4 right-4 flex flex-col gap-2 z-[9999] pointer-events-none">
<FlToast
v-for="t in toasts"
:key="t.id"
:toast="t"
@dismiss="dismiss(t.id)"
/>
</div>
</template>
<script setup>
import { FlToast, useToasts } from '@veryrogue/winui-vue'
const { toasts, dismiss } = useToasts()
</script>Step 2 — push toasts from anywhere:
import { useToasts } from '@veryrogue/winui-vue'
const { push } = useToasts()
// Simple
push('success', 'Saved.')
// Full options shape
push('error', {
title: 'Sync failed',
body: 'Couldn\'t reach the server.',
subtitle: 'Will retry automatically.',
undoLabel: 'Undo',
onUndo: () => {},
actions: [{ label: 'Retry', onClick: () => {} }],
timeout: 4500, // ms; 0 = sticky
})The useToasts() store is global — every call returns the same toasts ref. So you mount the portal once, and any component / composable in the app can fire toasts without further wiring.
Compound APIs (in detail)
These are sets of small components that work together via Vue's provide / inject. The root holds the state; the children hook into it.
Carousel (compound)
<script setup lang="ts">
import {
Carousel, CarouselContent, CarouselItem,
CarouselNext, CarouselPrevious, CarouselDots,
} from '@veryrogue/winui-vue'
</script>
<template>
<Carousel :autoplay="3000" :loop="true" :draggable="true">
<CarouselContent>
<CarouselItem>Slide 1</CarouselItem>
<CarouselItem>Slide 2</CarouselItem>
<CarouselItem>Slide 3</CarouselItem>
</CarouselContent>
<CarouselPrevious placement="overlay" /> <!-- or "inline" -->
<CarouselNext />
<CarouselDots />
</Carousel>
</template>Popover (compound)
<script setup lang="ts">
import {
Popover, PopoverTrigger, PopoverContent, PopoverClose,
} from '@veryrogue/winui-vue'
</script>
<template>
<Popover placement="below" :modal="true">
<PopoverTrigger>
<fluent-button>Open</fluent-button>
</PopoverTrigger>
<PopoverContent>
<p>Anything here.</p>
<PopoverClose /> <!-- × close button (only useful in modal mode) -->
</PopoverContent>
</Popover>
</template>TagPicker (compound)
Declarative options instead of an :options prop. Each <TagPickerOption> self-registers with the parent on mount.
<script setup lang="ts">
import { ref } from 'vue'
import {
TagPicker, TagPickerInput, TagPickerList, TagPickerOption,
} from '@veryrogue/winui-vue'
const picked = ref<string[]>([])
</script>
<template>
<TagPicker v-model="picked">
<TagPickerInput placeholder="Add a tag…" />
<TagPickerList>
<TagPickerOption value="ts" label="TypeScript" />
<TagPickerOption value="vue" label="Vue" />
<TagPickerOption value="css" label="CSS" />
</TagPickerList>
</TagPicker>
</template>NavRail (compound)
<script setup lang="ts">
import { ref } from 'vue'
import { NavRail, NavRailItem } from '@veryrogue/winui-vue'
const active = ref('home')
</script>
<template>
<NavRail v-model="active">
<NavRailItem id="home" label="Home">
<template #icon>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M3 12l9-9 9 9M5 10v10h14V10" />
</svg>
</template>
</NavRailItem>
<NavRailItem id="settings" label="Settings" />
<NavRailItem id="archive" label="Archived" :disabled="true" />
</NavRail>
</template>Required Fluent components per Fl* widget
Some library components render <fluent-…> tags internally. If you use one of these, you must register the matching Fluent web component in main.ts or the inner element renders blank/unstyled:
| Library component | Required define.js |
|---|---|
| FlSearchBox | text-input |
| FlDatePicker | text-input |
| FlTimePicker | text-input |
| FlTabs | tab, tablist |
| FlTagPicker (data-driven) | badge, listbox, option |
| TagPicker / TagPickerList (compound) | badge, listbox, option |
| FlPopout (only the default trigger fallback) | button |
| FlAccordionItem, FlCalendar, FlCard, FlCarousel, FlNavRail, FlRouterView, FlScrollArea, FlSelect, FlSideNav, FlToast, Carousel*, Popover*, NavRail* | none |
Example — if you only use FlSearchBox, FlTabs, and the toast portal:
import '@fluentui/web-components/text-input/define.js' // for FlSearchBox
import '@fluentui/web-components/tab/define.js' // for FlTabs
import '@fluentui/web-components/tablist/define.js' // for FlTabsNative Fluent UI v3 components
Anything starting with <fluent-…> is a Microsoft web component. They're not exported from this library — register them yourself in main.ts from @fluentui/web-components/<name>/define.js. Full catalog of the ones you might want in your own templates:
// In main.ts, only import the ones you actually use
import '@fluentui/web-components/accordion/define.js'
import '@fluentui/web-components/accordion-item/define.js'
import '@fluentui/web-components/anchor-button/define.js'
import '@fluentui/web-components/avatar/define.js'
import '@fluentui/web-components/badge/define.js'
import '@fluentui/web-components/button/define.js'
import '@fluentui/web-components/checkbox/define.js'
import '@fluentui/web-components/compound-button/define.js'
import '@fluentui/web-components/counter-badge/define.js'
import '@fluentui/web-components/dialog/define.js'
import '@fluentui/web-components/dialog-body/define.js'
import '@fluentui/web-components/divider/define.js'
import '@fluentui/web-components/drawer/define.js'
import '@fluentui/web-components/drawer-body/define.js'
import '@fluentui/web-components/dropdown/define.js'
import '@fluentui/web-components/field/define.js'
import '@fluentui/web-components/image/define.js'
import '@fluentui/web-components/label/define.js'
import '@fluentui/web-components/link/define.js'
import '@fluentui/web-components/listbox/define.js'
import '@fluentui/web-components/menu/define.js'
import '@fluentui/web-components/menu-button/define.js'
import '@fluentui/web-components/menu-item/define.js'
import '@fluentui/web-components/menu-list/define.js'
import '@fluentui/web-components/message-bar/define.js'
import '@fluentui/web-components/option/define.js'
import '@fluentui/web-components/progress-bar/define.js'
import '@fluentui/web-components/radio/define.js'
import '@fluentui/web-components/radio-group/define.js'
import '@fluentui/web-components/rating-display/define.js'
import '@fluentui/web-components/slider/define.js'
import '@fluentui/web-components/spinner/define.js'
import '@fluentui/web-components/switch/define.js'
import '@fluentui/web-components/tab/define.js'
import '@fluentui/web-components/tablist/define.js'
import '@fluentui/web-components/text/define.js'
import '@fluentui/web-components/text-input/define.js'
import '@fluentui/web-components/textarea/define.js'
import '@fluentui/web-components/toggle-button/define.js'
import '@fluentui/web-components/tooltip/define.js'
import '@fluentui/web-components/tree/define.js'
import '@fluentui/web-components/tree-item/define.js'Once registered, they're plain HTML tags in any template:
<fluent-button appearance="primary">Save</fluent-button>
<fluent-badge color="success">Live</fluent-badge>
<fluent-text size="500" weight="semibold">Heading</fluent-text>
<fluent-card class="!p-4">…</fluent-card>
<fluent-progress-bar value="70" max="100" />Full reference for every native component lives at https://fluent2.microsoft.design/components/web/react.
Tailwind aliases
Once you've imported @veryrogue/winui-vue/style.css, every Fluent design token is available as a Tailwind utility:
| Family | Examples |
|---|---|
| Colors | bg-bg-1, text-fg-2, border-stroke-2, bg-brand, bg-subtle-hover, bg-overlay, bg-pal-green |
| Shadows | shadow-fl-2, shadow-fl-4, shadow-fl-8, shadow-fl-16, shadow-fl-28, shadow-fl-64 |
| Radius | rounded-fl-sm, rounded-fl-md, rounded-fl-lg, rounded-fl-xl, rounded-fl-circular |
| Spacing | p-fl-m, gap-fl-l, mt-fl-xl, px-fl-xxl (and every Fluent step in between) |
| Type | text-fl-300, text-fl-hero-700, font-fl-semibold, leading-fl-300 |
Local development
git clone https://github.com/Duplore/winui-vue.git
cd winui-vue
npm install
npm run dev # gallery + per-component docs at http://localhost:5173
npm run build # builds the publishable library to dist/License
MIT
