UI Components

Svelar projects come with two UI layers out of the box:

  1. 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.

  2. 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.

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:

  1. Create the .svelte file in packages/svelar/src/ui/
  2. Re-export it from packages/svelar/src/ui/index.ts
  3. 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 class prop for style overrides
  • Spread ...rest props for flexibility
  • Use --color-brand CSS variables for theming
  • Keep components small and composable

Next Steps


Svelar UI Components Guide © 2026

Svelar © 2026 · MIT License