npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@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

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 aliasesbg-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/vite

PowerShell — 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/vite

cmd.exe — same as PowerShell, single line:

npm install @veryrogue/winui-vue @fluentui/web-components @fluentui/tokens @microsoft/fast-element vue tailwindcss @tailwindcss/vite

Setup

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 @import and @source? theme.css defines the design-token aliases (so utilities like bg-bg-1 exist). @source tells 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: importwhat it isminimal example.

Each snippet below is fully self-contained — copy the entire .vue block, 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, and FlTimePicker default to width: 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 NavRailItem accept an #icon slot — 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 scale
  • orientation'vertical' (default) | 'horizontal'
  • shadow'none' | 'sm' (default) | 'md' | 'lg' | 'xl'
  • interactive — boolean. Auto-enabled when href, to, or as="button" is set
  • selected — boolean. Toggles a brand-colored ring; emits select on click
  • disabled — boolean
  • title, description — convenience text props (or use #title / #description slots)
  • media, mediaAlt — convenience image URL (or use #media slot 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) for left/right
  • href — 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: absolute and stacked on top, fading out fast). The component wraps <router-view> in its own position: relative; overflow: hidden stage, so you don't need to add a wrapper. Pages get a default --colorNeutralBackground1 background 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:

  • itemsFlSideNavItem[] (single section) or FlSideNavSection[] (grouped with section titles)
  • title — brand text in the header (or use #header slot)
  • appearance'filled' (default) | 'subtle' | 'transparent'
  • width — px or CSS value when expanded (default 260)
  • collapsedWidth — px or CSS value in rail mode (default 56)
  • collapsed — boolean. Desktop-only icon rail
  • mobileBreakpoint — px viewport below which it switches to drawer (default 768)
  • closeOnNavigate — auto-close drawer on route change (default true)
  • 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 (default true). NavRail-style sliding indicator bar that smoothly glides between active items. Set false for no indicator
  • indicatorHeight — px height of the sliding bar (default 16)
  • density'compact' | 'comfortable' (default) | 'spacious'. Vertical rhythm preset
  • itemGap — px or CSS length. Override the gap between rows
  • itemPaddingY — px or CSS length. Override row vertical padding
  • indicatorColor — any CSS color ('#3b82f6', 'oklch(70% 0.2 240)', 'var(--my-token)'). Recolors the sliding bar
  • indicatorWidth — px or CSS length. Width of the sliding bar (default 3px)

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 to collapsedWidth |

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 — chrome
  • icon{ item, active } — replace just the leading icon
  • label{ item, active } — replace just the label text
  • badge{ item, active } — replace just the trailing chip
  • item{ item, active, collapsed, props }full escape hatch. Render anything; props includes the bound ref, class, and onClick so 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.prevent causes a full page reload in SPAs (the URL ends up like /about#/ with hash routing). Use <router-link> or pair the <a> with router.push as 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: fixed automatically; the hamburger toggle floats top-left at 12px / 12px. Override with toggle-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:

  • modelValuestring (single) or string[] (multi). Use with v-model
  • optionsFlSelectOption[] ({ value, label, disabled?, group? }) or plain string[] for label-only options
  • multiple — boolean. Toggle to select more than one
  • searchable — boolean. Adds a filter input at the top of the dropdown
  • clearable — boolean. Shows a × in the trigger when there's a value
  • placeholder — 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 state
  • size'small' | 'medium' (default) | 'large'
  • appearance'outline' (default) | 'filled' | 'underline'
  • maxHeight — px or CSS length, max panel height (default 280)
  • 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.

  1. Mount the toast portal once in your root component (usually App.vue).
  2. 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 FlTabs

Native 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