A comprehensive Svelte wrapper library for Microsoft FluentUI web components (v2.6.x), providing a seamless way to use FluentUI components in Svelte applications.
InputFile chips mode now has an overall progress footer — the same Pause all / Resume all / Retry all controls plus the aggregate progress bar that list/popover modes already had. Works for both chipsPosition values; rendered as a sibling below the chips so layout stays sane regardless of position.package.json homepage points at the docs site — npm's "Homepage" sidebar link now goes to svelte-fluentui.keenmate.dev instead of duplicating the GitHub repo link. Two distinct destinations on the package page./components/inputfile remembers your last configuration across page reloads. Files themselves are not persisted (File objects don't survive JSON).InputFile — appearance split into selectorAppearance + listAppearance + new cardSize prop — the single appearance union baked the trigger visual and the file-list visual together, so combinations like "button trigger with popover list" or "card with chips below" weren't expressible. Now selectorAppearance: "card" | "button" | "minimal" and listAppearance: "list" | "chips" | "popover" | "none" are independent. All previous presets still expressible (card + list, button + list, minimal + popover, card + chips). New cardSize: "minimal" | "compact" | "big" resizes the card — big is the original tall vertical card, compact is a 2-row grid layout for forms where 200px felt enormous, and minimal collapses everything to a single inline wrapping row (icon · message · hint · hint · Browse) with · separators between hintsInputFile — expandOnDrag + expandOnDragTarget props for drag-expanded overlay — when an external file is dragged onto the page, the card temporarily expands into a larger portaled overlay so the user has a generous hit area. Rendered above the form via use:portal so the surrounding layout doesn't shift up and down. Overlay centre is anchored to the selector's (or expandOnDragTarget's) centre via transform: translate(-50%, -50%) and grows symmetrically in all four directions — clamped to 90vw / 90vh so it never escapes the viewport. Dismissal paths cover Esc keydown (since OS-originated drags don't reliably fire dragend on cancel), cursor leaving the viewport (document.dragleave with relatedTarget === null), drop, and window blur. The overlay's own dragleave uses relatedTarget containment rather than target === currentTarget to avoid a flicker bug (dragleave fires when moving from a parent element into a child, same as mouseout)InputFile — popover sticky header + sticky footer with progress bar, batch actions, and three CSS-variable dimensions — Long file lists in minimal-mode popovers used to scroll the header (file count, +Add more, Clear all) away as the user navigated; aggregate progress was nowhere visible without picking through individual rows. New sticky header pins the file count + actions + limits hint to the top; new sticky footer carries an overall progress bar (uploadedBytes / totalBytes), a stats line via labels.totalProgress({completed, failed, total, uploadedBytes, totalBytes, percent}), and conditional batch buttons — Pause all (any uploading), Resume all (any paused), Retry all (any failed). New exported instance methods pauseAll() / resumeAll() / retryAll() and actions snippet payload widened to receive them. Popover sizing is now bracketed by three CSS variables: --fluent-inputfile-popover-width (default 28rem), --fluent-inputfile-popover-min-height (default 16rem), --fluent-inputfile-popover-max-height (default 24rem) — overridable per-theme or per-instance so the popover doesn't jump around as files are addedInputFile — FileUploadResult return type + onItemRemove(item) callback for clean server-side upload/cleanup flows — Previously the upload handler's Promise<void> return meant capturing a server-returned guid required a WeakMap + post-success onFileUploaded patch (fragile, two-step). Handler return widened to Promise<void | FileUploadResult> where FileUploadResult = Partial<Pick<InputFileItem, "metadata" | "downloadUrl" | "thumbnailUrl" | "name">> — restricted on purpose so handlers can't clobber id / status / progress. Returned fields are spread into the same patchItem that flips status to "completed", landing the server guid atomically with the completion flag. New onItemRemove?(item) callback fires once per item from removeAt / removeById / row × / clear() / reset(), receiving the removed item with its final metadata still intact. Together they cover the canonical scenario: file picked → auto-upload → server returns guid (now on item.metadata) → user removes the file before submit → consumer fires DELETE /api/files/:guid from onItemRemove. Demo 16 on /components/inputfile shows the full flow with an in-memory fake server + activity logInputFile — chipsPosition: "end" | "below" + chip restructure to [info | X] with full-height remove hitbox — When listAppearance="chips", the chips previously rendered at the trailing edge of the same flex row as the selector card, leaving a tall card + short chips visually floating mid-row. New chipsPosition prop (default "below") flips the layout: "end" keeps trailing-edge with an inner .chips-wrap so multi-row wrap doesn't anchor each row to the card's vertical centre, "below" stacks chips beneath the card. Chip itself restructured into .chip-info (filename / size / progress, fills width, non-interactive) and .chip-remove (full-height button covering everything from the info edge to the chip's right edge), so users no longer miss the small × and click empty chip spaceInputFile playground demo on /components/inputfile — live controls for selectorAppearance / listAppearance / cardSize / chipsPosition / multiple / disabled / expandOnDrag at the top of the examples grid so consumers can try every combination without rewriting codeborder-radius declarations across 12 components were swept into --fluent-border-radius-{sm,md,lg,xl,pill,circle} (2/4/6/8/999/50%) on :root, backed by matching SCSS tokens ($border-radius-sm/md/lg/xl/pill/circle). The pre-existing --fluent-border-radius is preserved as a back-compat alias of md. Pixel-identical to the prior visual — drove tier choice off existing values per call site (4px → md, 2px → sm, etc.). Theming the library's entire corner-rounding is now a six-line variable override instead of a 40-line search-and-replaceInputFile popover max-height was being silently overridden — fixed — Floating UI's size middleware ran on every reposition and unconditionally wrote floating.style.maxHeight = ${availableHeight - 16}px, so on any window taller than the CSS cap the inline style won and the popover ballooned to ~viewport height (sticky footer ended up pinned far below the visible scroll region, looking like it was floating mid-list). Middleware now clears the inline cap, reads the CSS-rule-driven max-height via getComputedStyle, and only re-applies an inline cap when the viewport is actually tighter than the ceiling. CSS variable wins on tall screens, viewport wins on short screensTabs with scroll/wrap/menu modes)document.body so they escape ancestor stacking contexts; every overlay uses a shared --fluent-z-* token scale with baked-in fallbacksportal action - Utility action exported for consumer-built overlays that need to escape parent stacking contextswindow.components["svelte-fluentui"].version() exposed at runtime for introspection (mirrors the pattern used by sister packages).fluent-label class so labels look identical across every field--base-height-multiplier × --design-unit tokensCtrl+Space (or ⌘+Space on Mac) to force-open the dropdown with all available optionstitle / header slot, right-aligned footer, primaryAction / secondaryAction shorthand, or custom footer snippet. Portaled to <body> so it escapes ancestor stacking contextsEscape dismissal, minDate/maxDate / minTime/maxTime, firstDayOfWeek, disabledDateFunc, autoClose, bindable openfirstDayOfWeek override, selectableDates allow-list, onPickerMonthChange callbackresponsive="scroll" | "wrap" | "menu" — scroll auto-shows ‹ › arrow buttons on overflow; menu collapses overflow into a ⋯ dropdown that swaps the picked tab into the strip on select, with ellipsis truncation on the borderline tab. Full keyboard nav, vertical orientation with stripWidth sidebar sizing + auto-ellipsis labels, swipe-to-navigate on the panels (opt out via swipe={false}), per-tab title tooltips, and justify for equal-width stretchingonclick. Takes items: MenuButtonItem[] (label, icon, disabled, visible, danger, dividerBefore, onclick), forwards button props, exposes bindable open. Positioned with Floating UI — auto-flips when near viewport edges, shifts to stay in bounds, and caps height with internal scroll when tight. position="bottom" | "top" controls the preferred side<ContextMenu items={…}>…</ContextMenu> and right-click inside opens a Floating-UI-positioned menu. Supports inline expandable sections (children + expandable: true, with a rotating chevron) for sidebar-style nav menus and side-opening submenus (nested children) with hover-intent delays, auto-flip away from the viewport edge, and arbitrary nesting. offsetMenuX / offsetMenuY push the menu away from the cursor so the click position doesn't land on the first item. Closes on scroll, click-outside, or Escape (Escape pops submenus one level at a time)bind:value, min/max/step, orientation, onchange/oninput, labelstoast.success(), toast.error(), etc. - 6 positions, progress bars, auto-dismissidMembernpm install svelte-fluentui
<script>
import { Button, TextField, Card } from 'svelte-fluentui'
</script>
<Card>
<TextField placeholder="Enter your name" />
<Button appearance="accent">Submit</Button>
</Card>
Button - Various button styles and appearancesTextField - Text input with validationNumberField - Numeric input controlTextarea - Multi-line text inputCheckbox - Checkbox input with three-state supportRadio / RadioGroup - Radio button controlsSwitch - Toggle switchSelect - Dropdown selectionCombobox - Searchable dropdownAutocomplete - Multiple selection with tags/chips (inspired by FluentUI Blazor)Slider - Range slider controlSearch - Search input fieldDatePicker - Date selection with calendar popup (inspired by FluentUI Blazor)TimePicker - Time selection with hour/minute/second picker (inspired by FluentUI Blazor)InputFile - File upload with drag-drop and progress tracking (inspired by FluentUI Blazor)DataGrid / DataGridRow / DataGridCell - Data table componentsQuickGrid - Advanced data grid with sorting, filtering, pagination, inline editing, tree mode, and custom filter predicatesCard - Content containerBadge - Status indicatorsProgressBar - Progress indicationTooltip - Contextual informationCalendar - Date picker and calendarPaginator - Pagination controlTabs / Tab / TabPanel - Tab navigationBreadcrumb / BreadcrumbItem - Breadcrumb navigationMenu / MenuItem - Context menusAppBar / AppBarItem - Application barNavMenu / NavItem / NavLink - Navigation componentsAnchor - Link componentStack - Flexible layout containerLayout - Page layout wrapperHeader / Footer / BodyContent - Layout sectionsSpacer - Spacing utilityDivider - Visual separatorDialog - Modal dialogsToast - Declarative notification messagesToastContainer + toast - Programmatic toast service (success, error, warning, info)Accordion / AccordionItem - Collapsible contentListbox / Option - List selectionTree / TreeItem - Hierarchical dataToolbar - Action toolbars<script>
import { TextField, Button, Stack } from 'svelte-fluentui'
let email = ''
let password = ''
</script>
<Stack orientation="vertical" gap="16">
<TextField
bind:value={email}
type="email"
placeholder="Enter email"
required
/>
<TextField
bind:value={password}
type="password"
placeholder="Enter password"
required
/>
<Button appearance="accent">Sign In</Button>
</Stack>
<script>
import { DataGrid, DataGridRow, DataGridCell } from 'svelte-fluentui'
const users = [
{ name: 'Alice', email: '[email protected]', role: 'Admin' },
{ name: 'Bob', email: '[email protected]', role: 'User' }
]
</script>
<DataGrid>
{#each users as user}
<DataGridRow>
<DataGridCell>{user.name}</DataGridCell>
<DataGridCell>{user.email}</DataGridCell>
<DataGridCell>{user.role}</DataGridCell>
</DataGridRow>
{/each}
</DataGrid>
<script lang="ts">
import { QuickGrid } from 'svelte-fluentui'
type User = {
id: number
name: string
email: string
role: string
}
const users: User[] = [
{ id: 1, name: 'Alice', email: '[email protected]', role: 'Admin' },
{ id: 2, name: 'Bob', email: '[email protected]', role: 'User' },
{ id: 3, name: 'Carol', email: '[email protected]', role: 'User' }
]
const columns = [
{ field: 'id', title: 'ID', width: '80px', sortable: true },
{ field: 'name', title: 'Name', sortable: true, filterable: true },
{ field: 'email', title: 'Email', filterable: true },
{ field: 'role', title: 'Role', sortable: true }
]
</script>
<QuickGrid
items={users}
{columns}
sortable
filterable
pageable
pageSize={10}
/>
<script>
import { Layout, Header, NavMenu, NavGroup, NavLinkItem, BodyContent } from 'svelte-fluentui'
import { page } from '$app/stores'
// Check if a link is active based on current route
function isActive(href: string): boolean {
if (!href) return false
if (href === "/" && $page.url.pathname === "/") return true
if (href !== "/" && $page.url.pathname.startsWith(href)) return true
return false
}
</script>
<Layout>
<Header slot="header">
<h1>My App</h1>
</Header>
<NavMenu slot="navigation">
<NavGroup title="Main Menu">
{#snippet linkText()}
Main Menu
{/snippet}
<NavLinkItem href="/" class={isActive("/") ? "active" : ""}>
Home
</NavLinkItem>
<NavLinkItem href="/about" class={isActive("/about") ? "active" : ""}>
About
</NavLinkItem>
<NavLinkItem href="/contact" class={isActive("/contact") ? "active" : ""}>
Contact
</NavLinkItem>
</NavGroup>
</NavMenu>
<BodyContent>
<!-- Main content here -->
</BodyContent>
</Layout>
<script>
import { DatePicker, TimePicker } from 'svelte-fluentui'
let selectedDate = $state<Date | null>(new Date())
let selectedTime = $state<string | null>("14:30")
// Deny weekends
const disabledDateFunc = (d: Date) => d.getDay() === 0 || d.getDay() === 6
</script>
<DatePicker
bind:value={selectedDate}
label="Select date"
placeholder="Choose a date"
firstDayOfWeek={1}
{disabledDateFunc}
autoClose={true}
/>
<TimePicker
bind:value={selectedTime}
label="Select time"
useAmPm={false}
showSeconds={false}
minTime="08:00"
maxTime="18:00"
autoClose={true}
/>
<script>
import { Dialog, Button } from 'svelte-fluentui'
let open = $state(false)
let name = $state("")
</script>
<Button onclick={() => open = true}>Open Dialog</Button>
<Dialog
bind:visible={open}
title="Rename item"
modal
primaryAction={{
label: "Save",
onClick: async () => {
if (!name.trim()) return false // keep open
await saveName(name)
// auto-closes when onClick resolves to anything but `false`
}
}}
secondaryAction={{
label: "Cancel"
}}
>
<label>
Name
<input bind:value={name} />
</label>
</Dialog>
<script>
import { Tabs, Tab } from 'svelte-fluentui'
</script>
<!-- Default: tabs scroll horizontally when too wide -->
<Tabs activeId="overview">
{#snippet childContent()}
<Tab id="overview" label="Overview" />
<Tab id="members" label="Members" />
<Tab id="activity" label="Activity" />
<Tab id="integrations" label="Integrations" />
<Tab id="settings" label="Settings" />
{/snippet}
</Tabs>
<!-- Wrap onto multiple rows instead of scrolling -->
<Tabs responsive="wrap" activeId="a">...</Tabs>
<!-- Justify: tabs divide the row equally -->
<Tabs justify activeId="a">...</Tabs>
<script lang="ts">
import { Autocomplete } from 'svelte-fluentui'
const options = [
{ value: "1", text: "Option 1" },
{ value: "2", text: "Option 2" },
{ value: "3", text: "Option 3" }
]
let selected = $state<string[]>([])
</script>
<Autocomplete
bind:selectedOptions={selected}
options={options}
label="Select multiple"
placeholder="Type to search..."
maxSelectedOptions={5}
/>
<script lang="ts">
import { InputFile } from 'svelte-fluentui'
import type { FileUploadHandler } from 'svelte-fluentui'
const uploadFile: FileUploadHandler = async (file, onProgress) => {
const formData = new FormData()
formData.append('file', file)
// Your upload logic here
// Call onProgress(percent) to update progress bar
const response = await fetch('/api/upload', {
method: 'POST',
body: formData
})
if (!response.ok) throw new Error('Upload failed')
}
</script>
<InputFile
multiple={true}
accept="image/*"
maxFileSize={5 * 1024 * 1024}
uploadFileCallback={uploadFile}
onFileUploaded={(file) => console.log('Uploaded:', file.name)}
/>
<script>
import { ToastContainer, toast } from 'svelte-fluentui'
function showSuccess() {
toast.success('Operation completed successfully!')
}
function showError() {
const id = toast.error('Something went wrong', {
persistent: true,
position: 'top-center'
})
// Later: toast.dismiss(id)
}
function showWithProgress() {
toast.info('Processing your request...', {
showProgress: true,
duration: 8000
})
}
</script>
<!-- Add once in your layout -->
<ToastContainer />
<button onclick={showSuccess}>Show Success</button>
<button onclick={showError}>Show Error</button>
<button onclick={showWithProgress}>Show With Progress</button>
portal Action (for custom overlays)<script>
import { portal } from 'svelte-fluentui'
let open = $state(false)
</script>
<button onclick={() => open = true}>Show overlay</button>
{#if open}
<!-- Moved to <body> for the lifetime of this block so ancestor
stacking contexts / transforms don't affect its z-index. -->
<div use:portal class="my-overlay" onclick={() => open = false}>
My custom overlay
</div>
{/if}
// In any browser console, after the page has loaded svelte-fluentui:
window.components["svelte-fluentui"].version()
// => "1.0.0-rc08"
Also available as a direct import:
import { VERSION } from 'svelte-fluentui'
console.log(VERSION)
All overlays use a shared token scale so the layering stays consistent across the library. Variables are exposed at :root when you import the SCSS bundle; every z-index: var(--fluent-z-*) declaration in components also includes the numeric fallback so things still stack correctly even without our SCSS loaded.
| Token | Value | Used for |
|---|---|---|
--fluent-z-dropdown |
1000 | Simple dropdowns, grid overlays |
--fluent-z-sticky |
1020 | Sticky app bars / headers |
--fluent-z-fixed |
1030 | Fixed sidebars / mobile nav |
--fluent-z-modal-backdrop |
1040 | Dialog overlay |
--fluent-z-modal |
1050 | Dialog itself |
--fluent-z-popover |
1060 | Calendar / time / autocomplete popups (above modal) |
--fluent-z-tooltip |
1070 | Tooltip |
--fluent-z-toast |
1080 | Toast |
Clone the repository and install dependencies:
git clone https://github.com/KeenMate/svelte-fluentui.git
cd svelte-fluentui
npm install
The library source code is in src/lib/. To package the library:
npm run package
The documentation site is in the docs/ folder as a separate SvelteKit project:
cd docs
npm install
npm run dev
Or use the Makefile:
make dev # Runs docs dev server
Visit http://localhost:5173 to see the component showcase and examples.
Build the library for publishing:
npm run build
Build documentation Docker image:
# With local source
make docker-build-docs
# With specific npm version
make docker-build-docs VERSION=1.0.0-rc03
Contributions are welcome! Please feel free to submit a Pull Request.
MIT © KeenMate
Built on top of Microsoft FluentUI Web Components v2.6.x.