UI Components
Svelar projects come with two UI layers out of the box:
shadcn-svelte (recommended) — A comprehensive component library with 50+ components (Dialog, Dropdown, Select, Combobox, Calendar, Sheet, Command Palette, etc.) pre-installed at
$lib/components/ui/. This is the recommended way to build your app's user-facing pages. You own the source files and can customize them freely.Svelar built-in components (
@beeblock/svelar/ui) — A minimal set of components used by the framework's dashboard and admin panel. These are framework-internal and should not need to be modified.
Not locked in. shadcn-svelte is the recommended default, but you can use any CSS framework or component library you prefer. Tailwind CSS v4 is included regardless.
shadcn-svelte (Recommended)
When you scaffold a project with npx @beeblock/svelar new, all shadcn-svelte components are installed automatically at $lib/components/ui/. If you scaffold with --no-install, run npm run ui:install after npm install; that script keeps Shadcn runtime packages in production dependencies for adapter-node builds. Use components in your pages:
<script>
import { Button } from '$lib/components/ui/button';
import * as Card from '$lib/components/ui/card';
import * as Dialog from '$lib/components/ui/dialog';
import { Input } from '$lib/components/ui/input';
import { Label } from '$lib/components/ui/label';
</script>
Dark mode is supported out of the box via mode-watcher. The cn() utility is available at $lib/utils.ts for conditional class merging.
For full component documentation, see shadcn-svelte.com.
Operational Patterns
Use make:ui to generate optional, app-owned Svelte 5 patterns built on the installed shadcn primitives. Run npm run ui:install first if those primitives are missing; the generator reports missing components instead of installing dependencies.
Generate all six with npx svelar make:ui all (or npx svelar make:ui without arguments). Generate a subset with npx svelar make:ui select server-table; comma-separated names are also accepted.
DDD output lives in src/lib/shared/components/patterns/, imported through $lib/shared/components/patterns. Flat apps use src/lib/components/patterns/ and $lib/components/patterns. Each selection includes shared contracts.ts and helpers.ts. Existing components are preserved by default. If existing shared contracts/helpers differ, generation stops before writing: review and merge customizations before using --force, which overwrites selected components and their shared files.
| CLI Name | Generated Component | Main Props And Bindings |
|---|---|---|
select |
SearchableSelect |
options: SelectOption[], label, bindable value; multiple, name, disabled, loading, error, onchange, onretry |
dialog |
ResponsiveDialog |
Bindable open, title, description, children snippet, optional footer snippet and pending |
tabs |
UrlTabs |
tabs: UrlTab[], label, queryKey (default tab), children(selectedValue) snippet; navigation uses the URL |
server-table |
ServerTable |
rows, columns: TableColumn<Row>[], getRowId, query: TableQuery, total, label, onquery; optional filters snippet, loading, error, onretry |
operation-tray |
OperationTray |
operations: OperationSummary[]; optional loading, error, onrefresh, oncancel(id), onretry(id) callbacks |
capability-editor |
CapabilityEditor |
catalog, scope, aliases, bindable value; mode (custom or system), defaults, bindable inclusions/exclusions, disabled, error, onchange |
Bind A Validated Select
This example assumes an authorized page/controller load returns a Superforms form and assigneeOptions shaped as SelectOption[] (value, label, optional disabled/keywords). Load options through the scoped application service/repository, not hardcoded browser records. The shared assignmentSchema defines a string assigneeId and is also used by the write FormRequest; its validated DTO goes to the action/service.
<script lang="ts">
import type { PageData } from './$types';
import { superForm } from 'sveltekit-superforms';
import { zod4Client } from 'sveltekit-superforms/adapters';
import { assignmentSchema } from '$lib/modules/tasks/contracts/schemas/assignment.schema.js';
import SearchableSelect from '$lib/shared/components/patterns/SearchableSelect.svelte';
import { Button } from '$lib/components/ui/button/index.js';
let { data }: { data: PageData } = $props();
const { form, errors, enhance, submitting } = superForm(data.form, {
validators: zod4Client(assignmentSchema), dataType: 'json',
});
</script>
<form method="POST" use:enhance novalidate>
<SearchableSelect options={data.assigneeOptions} label="Assignee" name="assigneeId"
bind:value={() => $form.assigneeId, (value) => { $form.assigneeId = typeof value === 'string' ? value : ''; }}
disabled={$submitting} error={$errors.assigneeId?.[0] ?? ''} />
<Button type="submit" disabled={$submitting}>Save</Button>
</form>
Use valibotClient with the same app-owned Valibot schema in a Valibot project; flat imports follow that app's schema layout. For multiple selection, define an array in the shared schema and bind that array with multiple. Keep the app's CSRF-enabled form setup and server action feedback. See Forms for shared-schema adapters, inline errors and Superforms submission handling. A disabled option or client validator does not authorize the selected record.
Load A Server Table
Have the authorized server load/controller return rows, total and query. Its tableQuery(event.url, allowedSortKeys) call parses bounded URL input; apply the resulting TableQuery through your service/repository with an explicit sortable-column allowlist and tenant/policy filters. The helper is not a database query or an authorization guard.
<script lang="ts">
import type { PageData } from './$types';
import { page } from '$app/state';
import { goto, invalidateAll } from '$app/navigation';
import ServerTable from '$lib/shared/components/patterns/ServerTable.svelte';
import type { TableColumn } from '$lib/shared/components/patterns/contracts.js';
import { tableUrl } from '$lib/shared/components/patterns/helpers.js';
let { data }: { data: PageData } = $props();
const columns: TableColumn<PageData['rows'][number]>[] = [
{ key: 'name', label: 'Name', text: (row) => row.name, sortable: true, truncate: true },
];
</script>
<ServerTable rows={data.rows} {columns} getRowId={(row) => row.id}
query={data.query} total={data.total} label="Records"
onquery={(query) => goto(tableUrl(page.url, query), { keepFocus: true, noScroll: true })}
onretry={() => invalidateAll()} />
TableQuery carries page, perPage, search, sort and direction; tableUrl() preserves unrelated query parameters. UrlTabs uses tabValue()/tabUrl() for reloadable tab selection. Revalidate parameters and permissions on every server navigation; browser URLs and column definitions are untrusted input.
Keep Operational State Server-Owned
OperationTray renders records; its callbacks do not issue API requests or run jobs. Load the current actor/tenant's durable records through BackgroundOperation.list() or the protected OperationsController. Register operation handlers and the recovery sweep in the common bootstrap. Authorize the private channel from BackgroundOperation.currentChannel(); an operation.changed SSE signal should trigger an authorized refetch, including after reconnect, rather than replace server state with a browser guess. Cancellation is a request, not immediate completion. Supply cancel/retry controls only when the corresponding guarded backend operation is implemented, retaining CSRF and failure feedback.
For CapabilityEditor, obtain permissionManifest.capabilities(scope), permissionManifest.aliases(scope) and the scoped PermissionProfiles.preview(roleId) from an authorized server load, not browser-supplied catalogs or scope. System mode presents defaults minus exclusions plus inclusions; custom mode presents an exact selection. Submit validated selections through a FormRequest/DTO/action and enforce scope, dependencies, protected identities and grant authority again on the backend. Selecting all team capabilities never grants platform ownership. See Permissions & Capabilities for manifests, profiles and backend guards; UI visibility and selections are presentation, not security.
Svelar Built-in Components
Svelar also ships a minimal, composable component library built on Svelte 5 runes used by the dashboard and admin panel. Every component is themed via Tailwind CSS v4 @theme tokens.
Available Components
Svelar includes: Button, Input, Label, Card (with CardHeader, CardTitle, CardDescription, CardContent, CardFooter), Alert, Badge, Separator, Avatar (with AvatarImage, AvatarFallback), Tabs (with TabsList, TabsTrigger, TabsContent), Icon, Toaster, toast notification API, and Seo (meta tags, Open Graph, Twitter Cards, JSON-LD). See the SEO guide for full <Seo> documentation.
Import
All components are available from the @beeblock/svelar/ui entry point:
<script>
import { Button, Card, CardContent, Input, Label } from '@beeblock/svelar/ui';
</script>
You can also import individual components directly:
<script>
import Button from '@beeblock/svelar/ui/Button.svelte';
</script>
Theming
Components use CSS custom properties for brand colors. Set them in your app.css:
:root {
--color-brand: #6366f1; /* Primary color (buttons, links, focus rings) */
--color-brand-dark: #4f46e5; /* Hover state for primary buttons */
}
This single change rebrands every component in your app.
Component Reference
Button
Supports 6 variants and 4 sizes:
<Button>Default</Button>
<Button variant="destructive">Delete</Button>
<Button variant="outline">Cancel</Button>
<Button variant="secondary">Secondary</Button>
<Button variant="ghost">Ghost</Button>
<Button variant="link">Link</Button>
<Button size="sm">Small</Button>
<Button size="lg">Large</Button>
<Button size="icon">🔍</Button>
<Button disabled>Disabled</Button>
<Button type="submit">Submit</Button>
<Button onclick={() => alert('clicked')}>Click Me</Button>
Input
Text input with bindable value:
<Input placeholder="Enter your name" bind:value={name} />
<Input type="email" required />
<Input type="password" disabled />
Label
Associates with form inputs:
<Label for="email">Email Address</Label>
<Input id="email" type="email" />
Card
Composable card with header, content, and footer slots:
<Card>
<CardHeader>
<CardTitle>Post Title</CardTitle>
<CardDescription>Published 2 days ago</CardDescription>
</CardHeader>
<CardContent>
<p>Card body content goes here.</p>
</CardContent>
<CardFooter>
<Button>Read More</Button>
</CardFooter>
</Card>
Alert
Status messages with 3 variants:
<Alert>Default informational alert.</Alert>
<Alert variant="destructive">Something went wrong.</Alert>
<Alert variant="success">Operation completed.</Alert>
Badge
Inline status indicators with 5 variants:
<Badge>Default</Badge>
<Badge variant="secondary">Draft</Badge>
<Badge variant="destructive">Error</Badge>
<Badge variant="outline">Outline</Badge>
<Badge variant="success">Published</Badge>
Avatar
User avatar with image and fallback:
<Avatar>
<AvatarImage src="/user.jpg" alt="John" />
<AvatarFallback>JD</AvatarFallback>
</Avatar>
Separator
Horizontal divider:
<Separator />
<Separator class="my-8" />
Tabs
Tab navigation with content panels:
<script lang="ts">
import { Tabs, TabsList, TabsTrigger, TabsContent } from '@beeblock/svelar/ui';
let activeTab = $state('overview');
</script>
<Tabs value={activeTab} onchange={(v) => (activeTab = v)}>
<TabsList>
<TabsTrigger value="overview">Overview</TabsTrigger>
<TabsTrigger value="settings">Settings</TabsTrigger>
</TabsList>
<TabsContent value="overview">Overview content</TabsContent>
<TabsContent value="settings">Settings content</TabsContent>
</Tabs>
Icon
A generic icon wrapper that supports @lucide/svelte and @tabler/icons-svelte components, raw SVG path data, or inline SVG children.
Important: Always import lucide icons individually. The barrel export (from '@lucide/svelte') causes SSR issues.
Generated apps install @lucide/svelte, so use individual imports such as @lucide/svelte/icons/users. Check your app's package.json for the installed package name instead of assuming the legacy lucide-svelte name.
<script lang="ts">
import { Icon } from '@beeblock/svelar/ui';
import Users from '@lucide/svelte/icons/users';
import KeyRound from '@lucide/svelte/icons/key-round';
</script>
<!-- Pass a Svelte icon component -->
<Icon icon={Users} size={20} />
<Icon icon={KeyRound} size={18} color="gray" />
<!-- Pass raw SVG path data -->
<Icon path="M5 12l5 5L20 7" size={24} strokeWidth={2} />
<!-- Inline SVG children -->
<Icon size={24}>
<circle cx="12" cy="12" r="10" />
</Icon>
Props: icon (Component), path (string), size (number, default 24), strokeWidth (number, default 2), color (string, default currentColor), class, aria-label.
Vite Configuration for Icon Libraries
Generated apps include Lucide icons. Tabler components are also supported by Icon, but are optional: run npm install @tabler/icons-svelte when your app uses them. The framework does not install the full Tabler catalogue for apps that do not need it.
When using @lucide/svelte or @tabler/icons-svelte, add these to your vite.config.ts:
export default defineConfig({
// ...
ssr: {
noExternal: ['@lucide/svelte'],
},
optimizeDeps: {
exclude: ['@lucide/svelte'],
},
});
Toast Notifications
Svelar includes a full toast notification system with animations, auto-dismiss, hover-to-pause, action buttons, and a progress bar.
Basic Usage
import { toast } from '@beeblock/svelar/ui';
toast('Hello');
toast.success('Saved!');
toast.error('Failed', { description: 'Network error' });
toast.warning('Careful', { duration: 8000 });
toast.info('Tip', { action: { label: 'Undo', onClick: () => undo() } });
Promise Toasts
Track async operations with loading → success/error:
toast.promise(fetch('/api/save'), {
loading: 'Saving...',
success: 'Saved!',
error: (err) => `Failed: ${err.message}`,
});
Dismissing Toasts
const id = toast('Manual dismiss', { duration: 0 }); // persistent
toast.dismiss(id); // dismiss one
toast.dismissAll(); // dismiss all
Toast Options
| Option | Type | Default | Description |
|---|---|---|---|
description |
string |
— | Secondary text below the title |
duration |
number |
5000 (8000 for errors) | Auto-dismiss in ms. 0 = persistent |
dismissible |
boolean |
true |
Show close button |
action |
{ label, onClick } |
— | Action button |
Rendering Toasts with Toaster
Add <Toaster /> to your root layout to render toast notifications:
<script>
import { Toaster } from '@beeblock/svelar/ui';
</script>
<Toaster position="bottom-right" maxVisible={5} />
Positions: top-right, top-left, bottom-right, bottom-left, top-center, bottom-center.
Default Icons
Each variant ships with a Lucide-inspired icon:
| Variant | Icon | Description |
|---|---|---|
success |
CircleCheck | Green circle with checkmark |
error |
CircleX | Red circle with X |
warning |
TriangleAlert | Amber triangle with exclamation |
info |
CircleInfo | Blue circle with "i" |
default |
— | No icon |
Customizing the Toaster
The <Toaster /> component accepts props for global and per-variant styling:
Global style props:
| Prop | Type | Description |
|---|---|---|
toastClass |
string |
Class added to every toast container |
titleClass |
string |
Class for the title text |
descriptionClass |
string |
Class for the description text |
actionClass |
string |
Class for the action button |
closeClass |
string |
Class for the dismiss button |
progressBarClass |
string |
Class for the countdown progress bar |
Per-variant overrides via the variants prop:
<script>
import { Toaster } from '@beeblock/svelar/ui';
import PartyPopper from '@lucide/svelte/icons/party-popper';
import Flame from '@lucide/svelte/icons/flame';
</script>
<Toaster
position="top-right"
toastClass="rounded-xl"
variants={{
success: {
icon: PartyPopper,
iconClass: 'text-green-700 bg-green-100 ring-green-300',
borderClass: 'border-l-green-600',
progressClass: 'bg-green-400',
},
error: {
icon: Flame,
iconClass: 'text-rose-700 bg-rose-100 ring-rose-300',
borderClass: 'border-l-rose-600',
containerClass: 'bg-rose-50',
},
}}
/>
Each variant accepts:
| Option | Type | Description |
|---|---|---|
icon |
Svelte Component | Custom icon component (receives size prop) |
iconClass |
string |
Classes for the icon badge (colors, background, ring) |
borderClass |
string |
Left border accent class |
progressClass |
string |
Progress bar color class |
containerClass |
string |
Additional classes on the toast container |
Cross-Package Reactivity Note
The toast store uses a callback-based pattern (not $state) so it works when imported from the svelar package. If you need fine-grained $state reactivity in your app, create a local wrapper:
// src/lib/stores/toasts.svelte.ts
import { getToasts, subscribe as toastSubscribe } from '@beeblock/svelar/ui';
export { toast, dismiss } from '@beeblock/svelar/ui';
let items = $state(getToasts());
toastSubscribe(() => { items = [...getToasts()]; });
export function toasts() { return items; }
Then use toasts() in your components for reactive rendering.
Extending the UI
Svelar's built-in components are intentionally minimal — they cover the patterns every app needs. For your app-specific UI, create custom components in your project that compose and extend svelar's base components.
Creating App-Specific Components
Create your custom components in src/lib/components/ and import svelar's base components:
<!-- src/lib/components/PostCard.svelte -->
<script lang="ts">
import { Card, CardHeader, CardTitle, CardContent, CardFooter, Badge, Button } from '@beeblock/svelar/ui';
interface Props {
title: string;
body: string;
published: boolean;
onDelete?: () => void;
}
let { title, body, published, onDelete }: Props = $props();
</script>
<Card class="hover:shadow-md transition-shadow">
<CardHeader>
<div class="flex items-center justify-between">
<CardTitle>{title}</CardTitle>
<Badge variant={published ? 'success' : 'secondary'}>
{published ? 'Published' : 'Draft'}
</Badge>
</div>
</CardHeader>
<CardContent>
<p class="text-gray-600">{body}</p>
</CardContent>
<CardFooter>
<Button variant="outline" size="sm">Edit</Button>
{#if onDelete}
<Button variant="destructive" size="sm" onclick={onDelete}>Delete</Button>
{/if}
</CardFooter>
</Card>
Use it in your pages:
<script>
import PostCard from '$lib/components/PostCard.svelte';
</script>
{#each posts as post}
<PostCard
title={post.title}
body={post.body}
published={post.published}
onDelete={() => deletePost(post.id)}
/>
{/each}
Building a Form Field Component
Compose Label, Input, and error display into a reusable form field:
<!-- src/lib/components/FormField.svelte -->
<script lang="ts">
import { Label, Input } from '@beeblock/svelar/ui';
interface Props {
label: string;
name: string;
type?: string;
value?: string;
error?: string;
placeholder?: string;
required?: boolean;
}
let {
label,
name,
type = 'text',
value = $bindable(''),
error,
placeholder = '',
required = false,
}: Props = $props();
</script>
<div class="space-y-2">
<Label for={name}>{label}</Label>
<Input
id={name}
{name}
{type}
{placeholder}
{required}
bind:value
class={error ? 'border-red-500' : ''}
aria-invalid={error ? 'true' : undefined}
/>
{#if error}
<p class="text-sm text-red-600">{error}</p>
{/if}
</div>
Creating a Confirmation Dialog
Build higher-level UI patterns on top of the base components:
<!-- src/lib/components/ConfirmDialog.svelte -->
<script lang="ts">
import { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter, Button } from '@beeblock/svelar/ui';
interface Props {
open: boolean;
title: string;
description?: string;
confirmLabel?: string;
cancelLabel?: string;
variant?: 'default' | 'destructive';
onConfirm: () => void;
onCancel: () => void;
}
let {
open = $bindable(false),
title,
description = '',
confirmLabel = 'Confirm',
cancelLabel = 'Cancel',
variant = 'default',
onConfirm,
onCancel,
}: Props = $props();
</script>
{#if open}
<div class="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
<Card class="w-full max-w-md">
<CardHeader>
<CardTitle>{title}</CardTitle>
{#if description}
<CardDescription>{description}</CardDescription>
{/if}
</CardHeader>
<CardFooter class="flex justify-end gap-2">
<Button variant="outline" onclick={onCancel}>{cancelLabel}</Button>
<Button variant={variant === 'destructive' ? 'destructive' : 'default'} onclick={onConfirm}>
{confirmLabel}
</Button>
</CardFooter>
</Card>
</div>
{/if}
Wrapping a Button with Loading State
<!-- src/lib/components/LoadingButton.svelte -->
<script lang="ts">
import { Button } from '@beeblock/svelar/ui';
import type { Snippet } from 'svelte';
interface Props {
loading?: boolean;
loadingText?: string;
children?: Snippet;
[key: string]: any;
}
let { loading = false, loadingText = 'Loading...', children, ...rest }: Props = $props();
</script>
<Button disabled={loading} {...rest}>
{#if loading}
<svg class="animate-spin -ml-1 mr-2 h-4 w-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path>
</svg>
{loadingText}
{:else if children}
{@render children()}
{/if}
</Button>
Customizing Component Styles
Every component accepts a class prop for Tailwind overrides:
<Button class="rounded-full px-8">Pill Button</Button>
<Card class="bg-gradient-to-r from-indigo-500 to-purple-600 text-white border-0">
<CardContent>Gradient card</CardContent>
</Card>
<Input class="text-lg py-3" placeholder="Large input" />
<Badge class="text-base px-4 py-1">Large Badge</Badge>
Adding New Base Components to Svelar Core
If you're building a component that every Svelar app would benefit from, add it to the framework itself:
- Create the
.sveltefile inpackages/svelar/src/ui/ - Re-export it from
packages/svelar/src/ui/index.ts - No build step needed — SvelteKit compiles Svelte source at dev/build time
// packages/svelar/src/ui/index.ts
export { default as Button } from './Button.svelte';
export { default as MyNewComponent } from './MyNewComponent.svelte';
// ...
Follow these conventions when adding core components:
- Use Svelte 5 runes (
$props(),$state(),$derived()) - Accept a
classprop for style overrides - Spread
...restprops for flexibility - Use
--color-brandCSS variables for theming - Keep components small and composable
Next Steps
- Learn about HTTP utilities for CSRF-aware API calls
- Set up Internationalization for multi-language support
- Explore Forms for validation with Superforms
Svelar UI Components Guide © 2026