A comprehensive Svelte wrapper library for Microsoft FluentUI web components (v2.6.x), providing a seamless way to use FluentUI components in Svelte applications.
Field component — wraps any form control with label + hint + validation message + state border. validationState: "none" | "warning" | "error" | "success" drives both the message color and the bottom-border accent on the inner control via shadow parts. hint renders neutral helper text when there's no error to show. Matches FluentUI 2 / Blazor convention of factoring validation presentation out of individual controlsValidationSummary component — top-of-form panel listing all errors with clickable jump-to-field links. Takes a plain errors: Record<string, string> map, auto-hides when empty, renders as role="alert" for screen readers. Pairs with <Field> for submit-time validation flows/components/forms/field covers all states, control types, live validation with touched-on-blur, horizontal orientation, and the submit-time summary pattern/applications/form-validation to use the new components — ~80 lines of inline <small class="field-error"> and hand-rolled summary panel removed; reads as a tutorial nowQuickGrid Ctrl+→ / Ctrl+← tree expand-collapse in navigate mode — keyboard-only tree traversal. Ctrl+→ expands the focused row; Ctrl+← collapses it (or walks up to the nearest expanded ancestor and collapses that). Modifier-key shape avoids ambiguity with plain ArrowLeft/Right which navigate columnsQuickGrid navigate mode: all cells are now focusable, not only editable ones — previously only editable cells got tabindex=0, so users couldn't land on read-only cells (like the tree column or parent rows in heterogeneous trees) and couldn't trigger Ctrl+arrow on them. Now every cell is focusable; Tab still walks through editable cells only (the productive Tab-through-fields UX). Focus indicator extended to all focused cellsQuickGrid Tab-while-editing now works in dblclick / click / button modes — only navigate mode had a Tab handler before; the others fell through to browser default and focus disappeared. Tab now commits + auto-opens the editor on the next editable cell (spreadsheet pattern)QuickGrid mode-switcher on the editable-per-row-type demo — /components/quickgrid-tree example gained a radio control to flip between navigate, dblclick, click, and button so you can verify keyboard behaviors across all modesQuickGrid predefined context-menu types for tree expand / collapse — contextMenu={["expand-all", "collapse-all", "expand-tree", "collapse-tree"]} wires common tree commands without writing handlers. expand-all / collapse-all operate on the right-clicked row's branch (file-explorer / IDE convention); expand-tree / collapse-tree operate on the entire dataset. Visibility gates auto-hide useless options (leaf rows don't show subtree commands, etc.). ContextMenuItem.label is now optional since predefined type supplies a defaultfindNextEditableCell helper walks past such rows in both directions_toast.scss partial existed and was complete (.fluent-toast-container, position variants, animations) but main.scss never @used it, so every consumer got the toast DOM with zero styling: an unstyled box at top-left of the viewport instead of the styled popover at top-right. Affects every page using the toast service. Fixed by adding @use "assets/styles/components/toast"; to main.scss/applications/form-validation walks through the common patterns: required & format, cross-field (passwords match), numeric range with custom rules, debounced async availability check, conditional required, and submit-time error summary with jump-to-field links. Includes a tiny composable validator helper recipe for copy-pastecolumn.isEditable (renamed from column.editable) now accepts a (row) => boolean callback so heterogeneous trees can express "only some row types are editable" without the consumer hand-rolling click handlers. Two shapes: per-column (isEditable: (row) => row.kind === "employee" — different columns can be editable for different row types) and grid-level (isRowEditable: (row) => boolean — single predicate gating the whole row). Three-stage gate: master editable → isRowEditable → column.isEditable. Navigate-mode Tab traversal is row-aware so the cursor skips read-only cells correctly. New demo at /components/quickgrid-tree ("Editable per row type — teams vs employees")column.nowrap prop — body cells never wrap. Combine with maxWidth for ellipsis truncation, with autoWidth to size the column to max(header, longest cell), or use alone for short identifiers like SKU/code columns. Demo on /components/quickgrid shows all three modescolumn.editable renamed to column.isEditable — boolean form is unchanged; the function form is additive. Migration: rename editable: true → isEditable: true on every column literal. Grid-level editable prop is unchanged--fluent-quickgrid-header-bg, -header-hover-bg, -header-sorted-bg, -stripe-bg, -row-hover-bg decouple QuickGrid surfaces from the generic --neutral-layer-* palette. Themes that overrode --neutral-layer-2 for navbar branding (e.g. DHL yellow) used to drag QuickGrid header + striped rows along at full saturation; consumers can now tint the grid independently▲ / ▼ triangle on the actively sorted column uses var(--accent-fill-rest) so theme switches propagate to itwidth / minWidth / maxWidth / columnMinWidth were applied only to <th>, so a wide body cell could drag the column past the header's max-width. Now mirrored to <td> via getColumnBodyStyle(). Fixes the case where ellipsis wouldn't activate on nowrap columnsstartIcon and endIcon snippets — decorative slots inside the input. startIcon renders before, endIcon renders after but yields to the auto-rendered clear button / loading spinner so a search icon swaps cleanly to a clear button on selection. New demo on /components/autocomplete--fluent-autocomplete-chip-* tokens (font-size, line-height, padding, gap, max-width, etc.) drive both inline and external chip geometry, defaults in rem. Override globally via :root { --fluent-autocomplete-chip-font-size: 0.75rem; }--fluent-autocomplete-chip-font-size token (default 0.875rem)selectOption was always appending, which silently wedged single-select mode after the second click (input went blank with two values held in state)PositioningRegion doesn't re-anchor on scroll, so scrolling the page used to leave the popover floating detached from its anchor. Now a capture-phase scroll listener closes it when scrolling outside the input or popover surfaceTabs active tab text tracks accent — .fluent-tab.active color changed to var(--accent-fill-rest) so the active label matches its bottom indicator instead of staying neutral graydocument.documentElement (data-theme, dir, --accent-fill-*, --foreground-on-accent-*, --neutral-layer-1, mirrored --fluent-* tokens). Theme mode 'system' resolves via matchMedia and re-applies on OS-level theme changes. Foreground-on-accent is computed via W3C luminance so dark accents get white labels and light accents (PowerBI yellow) get black labelstheme store localStorage key namespaced — "theme" → "fluent-theme" to prevent collisions with host applications using the same generic key (e.g. PureAdmin saves its theme name to localStorage.theme)Tabs 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.