Account & security settings console
A real SaaS settings surface composed from the UI kit: SvMenubar app bar, promise + Undo-action toasts on save, SvPopconfirm on destructive rows, SvHoverCard teammate previews, and frame input adornments (leading icons, prefix affixes, masked API key). Profile / Team / Security tabs over SvCard + SvStat.
A live, editable Svelte 5 component example from the SvGrid gallery (Layout). Read the SvGrid UI component docs for the full API.
About this example
A SaaS account and security settings console composed entirely from the SvGrid UI kit in Svelte 5. An SvMenubar app bar sits on top, Profile, Team and Security tabs render over SvCard and SvStat, saving fires a promise toast with an Undo action, destructive team rows ask through SvPopconfirm, teammates get SvHoverCard previews, and inputs use frame adornments: leading icons, prefix affixes and a masked API key.
Account & Security settings console - a real SaaS settings surface composed entirely from the SvGrid UI kit. It foregrounds the newest/upgraded pieces: SvMenubar app bar, promise + action (Undo) toasts on save, SvPopconfirm for destructive rows, SvHoverCard teammate previews, and frame input adornments (leading icons, prefix/suffix affixes, masked secrets).
Imports, features and API used
Imports: @svgrid/grid
Frequently asked questions
How does the Undo toast work?
The save handler shows a toast with an action button; clicking it within the toast's lifetime restores the previous values, which the demo keeps in a snapshot until the toast closes.
When should I use SvPopconfirm instead of a modal?
For low-stakes destructive actions where a full dialog would be heavy. It anchors to the button, asks one question and confirms or cancels in place.
How is the API key masked?
It is a text input with a leading icon and a masked value; a reveal action swaps the mask for the real key, the same pattern as the password input.
Related documentation
Related articles
- Saved Views - Persist Grid Layout and Filters - Give users named, switchable snapshots of column order, sorting, filters, and grouping - persisted to localStorage or a server adapter - using SvGrid's createNamedViews API.
- Avoiding Layout Thrash in Custom Grid Cells - Layout thrash from interleaved DOM reads and writes is the most common cause of scroll jank in grids with custom cells - here is how to find it and design your way out of it.
- Column Resizing and Reordering - Let Users Shape the Grid - How to wire drag-to-resize and drag-to-reorder in SvGrid, then persist the layout to localStorage so it survives page reloads.
Source code (337-account-settings-console.svelte)
<script lang="ts">
/**
* Account & Security settings console - a real SaaS settings surface composed
* entirely from the SvGrid UI kit. It foregrounds the newest/upgraded pieces:
* SvMenubar app bar, promise + action (Undo) toasts on save, SvPopconfirm for
* destructive rows, SvHoverCard teammate previews, and frame input adornments
* (leading icons, prefix/suffix affixes, masked secrets).
*/
import {
SvMenubar, SvTabs, SvCard, SvStat, SvTextInput, SvSwitchButton,
SvButton, SvBadge, SvAvatar, SvHoverCard, SvPopconfirm, SvDivider, SvToaster,
toast, type MenubarMenu, type MenuItem,
} from '@svgrid/grid'
// --- profile ---
let fullName = $state('Ada Lovelace')
let email = $state('[email protected]')
let company = $state('Northwind Analytics')
let site = $state('northwind.io')
// --- security ---
let twoFA = $state(true)
let sso = $state(false)
let sessionTimeout = $state(30)
let apiKey = $state('sk_live_4f8a92c1d7e6')
// --- team ---
type Member = { id: number; name: string; email: string; role: string; tasks: number; status: 'active' | 'invited' }
let members = $state<Member[]>([
{ id: 1, name: 'Ada Lovelace', email: '[email protected]', role: 'Owner', tasks: 12, status: 'active' },
{ id: 2, name: 'Grace Hopper', email: '[email protected]', role: 'Admin', tasks: 8, status: 'active' },
{ id: 3, name: 'Alan Turing', email: '[email protected]', role: 'Editor', tasks: 5, status: 'active' },
{ id: 4, name: 'Katherine Johnson', email: '[email protected]', role: 'Viewer', tasks: 0, status: 'invited' },
])
const menus: MenubarMenu[] = [
{ label: 'File', items: [
{ label: 'Export settings', onSelect: () => toast('Exported settings.json') },
{ label: 'Import...', onSelect: () => toast('Import cancelled') },
] },
{ label: 'Team', items: [
{ label: 'Invite member', shortcut: 'Ctrl+I', onSelect: inviteMember },
{ label: 'Manage roles', onSelect: () => toast('Opening roles') },
] },
{ label: 'Security', items: [
{ label: 'Rotate API key', onSelect: rotateKey },
{ label: 'View audit log', onSelect: () => toast('Opening audit log') },
] },
{ label: 'Help', items: [{ label: 'Docs', onSelect: () => toast('Opening docs') }, { label: 'Contact support', onSelect: () => toast('Support ticket started') }] },
]
const onMenu = (_: MenuItem) => {}
const tabs = [
{ id: 'profile', label: 'Profile' },
{ id: 'team', label: 'Team' },
{ id: 'security', label: 'Security' },
]
let tab = $state('profile')
// --- actions with promise + action toasts ---
function fakeSave(ms = 900, fail = false) {
return new Promise<void>((res, rej) => setTimeout(() => (fail ? rej(new Error('network')) : res()), ms))
}
function saveProfile() {
toast.promise(fakeSave(), {
loading: 'Saving profile...',
success: 'Profile saved',
error: (e) => `Could not save: ${(e as Error).message}`,
})
}
function rotateKey() {
const prev = apiKey
apiKey = 'sk_live_' + Math.round(performance.now()).toString(16).slice(0, 12)
toast('API key rotated', {
variant: 'warning',
action: { label: 'Undo', onClick: () => { apiKey = prev; toast.success('Key restored') } },
})
}
function inviteMember() {
const id = Math.max(0, ...members.map((m) => m.id)) + 1
members = [...members, { id, name: 'New Teammate', email: '[email protected]', role: 'Viewer', tasks: 0, status: 'invited' }]
toast.success('Invite sent')
}
function removeMember(m: Member) {
members = members.filter((x) => x.id !== m.id)
toast('Removed ' + m.name, {
action: { label: 'Undo', onClick: () => { members = [...members, m].sort((a, b) => a.id - b.id) } },
})
}
const activeCount = $derived(members.filter((m) => m.status === 'active').length)
</script>
<div class="wrap">
<SvMenubar {menus} onSelect={onMenu} ariaLabel="Settings menu" />
<div class="stats">
<SvCard><SvStat label="Seats used" value={`${activeCount} / 10`} hint="2 invites pending" /></SvCard>
<SvCard><SvStat label="API calls (24h)" value="18.2k" delta="+6%" trend="up" /></SvCard>
<SvCard><SvStat label="Storage" value="42 GB" hint="of 100 GB" /></SvCard>
<SvCard><SvStat label="Plan" value="Business" hint="Renews Aug 1" /></SvCard>
</div>
<SvTabs {tabs} bind:value={tab} />
{#if tab === 'profile'}
<SvCard title="Profile" subtitle="How your account appears to teammates.">
<div class="grid">
<SvTextInput label="Full name" bind:value={fullName} block>
{#snippet leading()}<span class="ic">👤</span>{/snippet}
</SvTextInput>
<SvTextInput label="Email" type="email" bind:value={email} block>
{#snippet leading()}<span class="ic">✉</span>{/snippet}
</SvTextInput>
<SvTextInput label="Company" bind:value={company} block />
<SvTextInput label="Website" bind:value={site} prefix="https://" block />
</div>
{#snippet footer()}
<div class="actions">
<SvButton variant="ghost">Cancel</SvButton>
<SvButton variant="primary" onclick={saveProfile}>Save changes</SvButton>
</div>
{/snippet}
</SvCard>
{:else if tab === 'team'}
<SvCard title="Team" subtitle="Invite teammates and manage their roles.">
<div class="card-head-action">
<SvButton size="sm" variant="outline" onclick={inviteMember}>Invite member</SvButton>
</div>
<ul class="members">
{#each members as m (m.id)}
<li>
<SvHoverCard placement="right">
{#snippet anchor()}<button class="who"><SvAvatar name={m.name} size="sm" />{m.name}</button>{/snippet}
<div class="prof">
<SvAvatar name={m.name} />
<div>
<strong>{m.name}</strong>
<div class="muted">{m.email}</div>
<div class="muted">{m.tasks} open tasks - {m.role}</div>
</div>
</div>
</SvHoverCard>
<span class="role"><SvBadge variant={m.role === 'Owner' ? 'accent' : 'neutral'}>{m.role}</SvBadge></span>
<span class="st">{#if m.status === 'invited'}<SvBadge variant="warning" dot>Invited</SvBadge>{:else}<SvBadge variant="success" dot>Active</SvBadge>{/if}</span>
<span class="rm">
{#if m.role !== 'Owner'}
<SvPopconfirm title={`Remove ${m.name}?`} description="They lose access immediately." confirmLabel="Remove" confirmVariant="danger" onConfirm={() => removeMember(m)}>
{#snippet anchor()}<SvButton size="sm" variant="ghost">Remove</SvButton>{/snippet}
</SvPopconfirm>
{/if}
</span>
</li>
{/each}
</ul>
</SvCard>
{:else}
<SvCard title="Security" subtitle="Protect your account and data.">
<div class="rows">
<div class="setting"><div><strong>Two-factor authentication</strong><div class="muted">Require a code at sign-in.</div></div><SvSwitchButton checked={twoFA} onChange={(v) => (twoFA = v)} /></div>
<SvDivider />
<div class="setting"><div><strong>Single sign-on (SSO)</strong><div class="muted">Let members sign in with your IdP.</div></div><SvSwitchButton checked={sso} onChange={(v) => (sso = v)} /></div>
<SvDivider />
<div class="setting">
<div><strong>API key</strong><div class="muted">Used for server-to-server calls.</div></div>
<div class="keyrow">
<SvTextInput value={apiKey} readonly>
{#snippet leading()}<span class="ic">🔑</span>{/snippet}
</SvTextInput>
<SvPopconfirm title="Rotate API key?" description="The current key stops working right away." confirmLabel="Rotate" confirmVariant="danger" onConfirm={rotateKey}>
{#snippet anchor()}<SvButton size="sm" variant="outline">Rotate</SvButton>{/snippet}
</SvPopconfirm>
</div>
</div>
</div>
</SvCard>
{/if}
</div>
<SvToaster position="bottom-right" />
<style>
.wrap { padding: 20px; display: flex; flex-direction: column; gap: 16px; max-width: 780px; }
.stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 12px; }
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 14px; }
.actions { display: flex; justify-content: flex-end; gap: 8px; }
.card-head-action { display: flex; justify-content: flex-end; }
.members { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; }
.members li { display: grid; grid-template-columns: 1fr auto auto auto; align-items: center; gap: 12px; padding: 9px 2px; border-bottom: 1px solid var(--sg-border, #eef2f7); }
.who { display: inline-flex; align-items: center; gap: 9px; background: none; border: 0; font: inherit; font-size: 13.5px; color: inherit; cursor: pointer; padding: 0; }
.prof { display: flex; gap: 12px; align-items: center; }
.prof strong { font-size: 14px; }
.muted { color: var(--sg-muted, #64748b); font-size: 12.5px; }
.rows { display: flex; flex-direction: column; }
.setting { display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 6px 0; }
.setting strong { font-size: 13.5px; }
.keyrow { display: flex; align-items: center; gap: 8px; }
.ic { display: inline-flex; font-size: 13px; }
</style>More Layout examples
- Invoice builder - An adornment-heavy money form: currency prefixes, % suffixes, masked tax IDs (frame adornments), line items with SvPopconfirm delete + Undo action toasts, live SvStat totals, an SvHoverCard tax hint, and a promise toast on Send. Pure UI-kit composition.
- Operations KPI dashboard - A KPI console: SvStat + SvSparkline tiles, an SvGauge SLA dial, SvHoverCard drill-down previews per service, an SvMenubar toolbar (View / Range / Actions) and a promise toast on Refresh. Pure UI-kit composition, no grid dependency.
- Form: rich field types - SvForm reaching the whole input suite from one schema - phone, country, mask, combobox, radio, slider, tags, datetime and file - plus per-field help text, a readonly field, column span, and a promise toast on submit.
- Form: cascading fields - SvForm dependent selects - a child list derives from the parent value via a function `options`, and `dependsOn` clears the child when the parent changes so a stale selection never lingers. Country -> State -> City.
- Layout & feedback primitives - The structural + status kit: SvStack / SvGroup / SvSimpleGrid layout primitives, SvCollapsible show/hide sections, SvSpinner + SvLoadingOverlay for async states, and SvResult for a full outcome page. All themed from the --sg-* tokens.