Widget System
The widget system is the foundation of our composable UI architecture, enabling dynamic application building through reusable, configurable components.
Overview
The widget system provides a powerful framework for building dynamic user interfaces through composable widgets. Each widget encapsulates specific functionality and can be configured, styled, and combined with other widgets to create complex applications.
graph TB
subgraph "Widget Ecosystem"
A[Widget Registry] --> B[Widget Definitions]
B --> C[Widget Components]
B --> D[Widget Schemas]
B --> E[Widget Categories]
F[Widget Tree] --> G[Root Widget]
G --> H[Child Widgets]
H --> I[Nested Widgets]
J[Widget Renderer] --> K[Component Loader]
K --> L[Dynamic Import]
L --> M[Svelte Component]
N[Widget Editor] --> O[Schema-based Forms]
O --> P[Visual Configuration]
P --> Q[Real-time Preview]
end
A --> F
F --> J
J --> NCore Concepts
Widget Definition
A widget is defined by several key components:
interface WidgetRegistration {
name: string; // Unique identifier
title?: InlineTranslation; // Localized display name
description?: InlineTranslation; // Localized widget description (required for catalog widgets)
category?: string; // Organization category
visibleInCatalog?: boolean; // Show in widget picker
componentLoader: () => Promise<any>; // Dynamic component loading
dataSchema?: () => Promise<any>; // Configuration schema
}
Important: Widget descriptions use
InlineTranslation(an object with language keys like{ en: "...", de: "..." }), not plain strings. Descriptions are required for widgets withvisibleInCatalog: true. They are displayed in the widget catalog and exposed via/docs/widgets/llm.txtfor AI agents and chatbots.
Widget Instance
Each widget instance contains:
interface Widget {
id: string; // Unique instance ID
name: string; // Widget type from registry
data?: any; // Widget configuration data
styles?: WidgetCSSStyles; // CSS styling
slots?: WidgetSlotMapping; // Child widget relationships
}
Widget Tree Structure
Widgets are organized in a hierarchical tree structure:
interface WidgetTree {
rootWidgetId: string | undefined; // Entry point
widgets: Record<string, Widget>; // All widget instances
}
Widget Categories
Layout Widgets
Widgets that provide structure and organization:
- Container: Horizontal or vertical layout container
- Max Width Container: Responsive width limiting
- App Shell: Full application layout with header, sidebar, and content
- Layout Child: Flexible layout positioning
Content Widgets
Widgets for displaying and editing content:
- Text: Rich text display and editing
- Image: Image display with responsive sizing
- Editor.js: Block-style content editor
- Hero: Large banner/hero sections
- Check-in (
CheckIn): Visitor check-in widget with registration form, optional QR code scanner, and GDPR-compliant data retention settings. See configuration details below. - Bunny Stream Video (
BunnyStreamVideo): Embeds a video from Bunny Stream (bunny.net) with configurable playback options. See configuration details below.
Form Widgets
Interactive input and form components:
- Form Fields: Various input types (text, number, date, etc.)
- Form Containers: Form layout and validation
- Button: Action triggers and navigation
- Login: User authentication widget
- Register: User registration widget
Data Widgets
Widgets for displaying and manipulating data:
- DataView: Sortable, filterable data display with saved queries
- EntityEditor: Edit entity records with validation
- Dashboard: Customizable dashboard with multiple widgets
- NumberStats: Display numeric statistics
- NumberStatsDataTypeCount: Count statistics by data type
- Todo: Task list and management
- ProfileCard: User profile display
- AddressCard: Address information display
- StarRating: Rating input and display
- Maps: Map display and location selection
Navigation Widgets
Components for application navigation:
- Navbar: Top-level navigation
- Footer: Bottom navigation and links
- Tabs: Tabbed content organization
- Breadcrumbs: Hierarchical navigation
Special Purpose Widgets
Specialized widgets for specific use cases:
- Kiosk: Full-screen kiosk mode for public displays
- EmoticonsBar: Emoji picker and emoticon input
- Error: Error display widget
EventStream Widgets
Real-time collaborative widgets for Event Streams:
- EventStreamMap: GPS location sharing with OpenStreetMap integration (docs)
- EventStreamImageGallery: Photo sharing with camera capture and gallery upload (docs)
- EventStreamChatMessages: Real-time messaging for event streams (docs)
- EventStreamTimeline: Chronological display of event entries (docs)
- EventStreamPinBoard: Collaborative pin board for notes and ideas (docs)
- EventStreamSettings: Configuration and settings for event streams (docs)
Website Building Block Widgets
Composable widgets for building public-facing website pages:
- WebsiteSection: Configurable page section wrapper with variant (
base,alternate,accent) and padding options (sm,md,lg,xl) - WebsiteNavigation: Fixed navigation bar with scroll-aware styling, responsive mobile menu, brand logo, navigation links, and CTA button
- WebsiteFooter: Footer with contact information, navigation links, and social media icons
- WebsiteFrame: Full-page wrapper combining WebsiteNavigation and WebsiteFooter around page content
- CardGrid: Responsive card grid layout with configurable columns (1-3) and style variants (
flat,elevated,gradient) - CtaSection: Call-to-action section with title, subtitle, button, and style variants (
subtle,gradient) - FaqAccordion: Expandable FAQ section with question/answer pairs
- FeatureList: Icon-based feature list for trust signals and feature highlights
- PricingTable: Responsive pricing tier comparison table with feature checklists and CTA buttons
Utility Widgets
Specialized utility widgets:
- QRCodeScanner: Real-time QR code and barcode scanning with configurable actions (docs)
- IpForwarder: Network utility for IP forwarding and port mapping (docs)
- PushNotificationSubscription: Browser notification subscription management
Bunny Stream Video Widget
Widget name constant: WIDGET_BUNNY_STREAM_VIDEO ("BunnyStreamVideo")
Category: content
Visible in catalog: yes
The Bunny Stream Video widget embeds a video hosted on Bunny Stream (bunny.net) using a responsive <iframe> with a standard 16:9 aspect ratio. Playback behaviour is fully configurable through widget properties that are appended as query parameters to the player URL.
How It Works
The widget takes a Bunny Stream player URL (e.g. https://player.mediadelivery.net/embed/...) and appends the configured playback options as query parameters before rendering it in a responsive <iframe>. If no URL is provided the widget renders nothing. If the URL cannot be parsed it is used as-is.
Configuration Properties
| Property | Type | Default | Description |
|---|---|---|---|
url |
string |
"" |
Bunny Stream player URL (e.g. https://player.mediadelivery.net/embed/...). The widget renders nothing if this is empty. |
autoplay |
boolean |
true |
Automatically starts playback when the page loads. |
loop |
boolean |
true |
Loops the video continuously after it ends. |
muted |
boolean |
true |
Mutes the video audio. Required by most browsers for autoplay to work. |
preload |
boolean |
true |
Instructs the player to preload video data. |
responsive |
boolean |
true |
Enables responsive sizing within the player. |
Example Configuration
// Widget data object for the BunnyStreamVideo widget
const bunnyStreamVideoWidgetData = {
url: "https://player.mediadelivery.net/embed/123456/abcdef-1234-5678-abcd-ef1234567890",
autoplay: true,
loop: true,
muted: true,
preload: true,
responsive: true
};
Notes
- The widget wraps the
<iframe>in aposition: relative; padding-top: 56.25%container to maintain a 16:9 aspect ratio at any container width. - The
<iframe>hasallow="accelerometer;gyroscope;autoplay;encrypted-media;picture-in-picture;"andallowfullscreenset by default. - The
<iframe>usesloading="lazy"for deferred loading. - Because
muteddefaults totrue, theautoplaydefault will work in all modern browsers without requiring user interaction.
Check-in Widget
Widget name constant: WIDGET_CHECKIN ("CheckIn")
Category: content
Visible in catalog: yes
The Check-in widget provides a self-service visitor registration kiosk that can be embedded on any page. It presents a registration form driven by a configurable visitor data type, optionally allows returning visitors to identify themselves via QR code scan, and enforces GDPR-compliant data retention by letting each visitor choose how long their data is stored.
How It Works
- Welcome screen — Displays the configured title and description with a prompt to register or scan a QR code.
- Registration form — Renders a schema-driven form based on the visitor data type. Internal fields (
latestEventType,latestEventAt,deleteOn,deleteOnPreference) are hidden from the visitor and populated automatically. - QR code scanner (optional) — Allows returning visitors to scan their visitor pass QR code to check out or update their record.
- Success screen — Shows a localized success message after check-in or check-out.
- Profile view — Allows the visitor to review or edit their registered data.
On submission the widget calls the server-side Visitors API (POST /api/projects/[projectId]/visitors) and records a corresponding visitor event (POST /api/projects/[projectId]/visitors/[visitorId]/events).
Configuration Properties
| Property | Type | Required | Description |
|---|---|---|---|
title |
InlineTranslation |
No | Heading shown on the welcome screen (e.g. { en: "Welcome", de: "Willkommen" }). |
description |
InlineTranslation |
No | Introductory text shown below the title on the welcome screen. |
successSignInMessage |
InlineTranslation |
No | Message displayed after a successful check-in. |
successSignOutMessage |
InlineTranslation |
No | Message displayed after a successful check-out. |
smallPrint |
InlineTranslation |
No | Privacy/legal notice shown at the bottom of the registration form (e.g. GDPR retention notice). |
scannerEnabled |
boolean |
No | When true, a QR code scanner button is shown so returning visitors can identify themselves by scanning their visitor pass. Defaults to false. |
scannerButtonText |
InlineTranslation |
No | Label for the QR code scanner button. Only shown when scannerEnabled is true. |
deletionPeriods |
DeletionPeriod[] |
No | List of retention period options presented to the visitor. Each entry has duration (milliseconds), label (InlineTranslation), and isDefault (boolean). The entry marked isDefault: true is pre-selected. If omitted, a default of 28 days (2 419 200 000 ms) is used. |
visitorTypeId |
string |
No | ID of the data type used to store visitor records. The widget derives the collection name and registration form schema from this type. |
visitorEventTypeId |
string |
No | ID of the data type used to store visitor events (check-in / check-out log entries). |
contactsTypeId |
string |
No | ID of the contacts data type, used to link a visitor to an internal contact person. |
DeletionPeriod object
interface DeletionPeriod {
duration: number; // Retention duration in milliseconds
label: InlineTranslation; // Displayed option label
isDefault: boolean; // Pre-select this option
}
Example Configuration
// Widget data object for the CheckIn widget
const checkinWidgetData = {
title: { en: "Welcome", de: "Willkommen" },
description: {
en: "Please register as a visitor",
de: "Bitte registriere dich als Besucher"
},
successSignInMessage: {
en: "You have been checked in successfully.",
de: "Du wurdest erfolgreich eingecheckt."
},
successSignOutMessage: {
en: "You have been checked out successfully.",
de: "Du wurdest erfolgreich ausgecheckt."
},
smallPrint: {
en: "Your data will be stored according to GDPR regulations and automatically deleted after the selected retention period.",
de: "Deine Daten werden gemäß DSGVO gespeichert und nach dem ausgewählten Aufbewahrungszeitraum automatisch gelöscht."
},
scannerEnabled: true,
scannerButtonText: { en: "Scan QR Code", de: "QR-Code scannen" },
deletionPeriods: [
{
duration: 604800000, // 7 days
label: { en: "7 days", de: "7 Tage" },
isDefault: false
},
{
duration: 2419200000, // 28 days
label: { en: "28 days", de: "28 Tage" },
isDefault: true
},
{
duration: 7776000000, // 90 days
label: { en: "90 days", de: "90 Tage" },
isDefault: false
}
],
visitorTypeId: "<visitor-type-id>",
visitorEventTypeId: "<visitor-event-type-id>",
contactsTypeId: "<contacts-type-id>"
};
Template Integration
The Check-in widget is the core UI component of the Check-in feature template (FeatureTemplateName.CHECKIN). When the template is applied the checkinAppExtension automatically creates an application containing a pre-configured CheckIn widget instance wired to the generated visitor and visitor-events data types. The template now also requires the CONTACT feature so that visitor records can reference an internal contact person.
Related extensions created by the template:
| Extension | Purpose |
|---|---|
checkinAppExtension |
Creates the check-in application with a pre-configured CheckIn widget. |
checkinGdprExtension |
Creates a nightly cron action that deletes visitor records whose deleteOn timestamp has passed. |
checkinNotificationsExtension |
Creates email actions for visitor passes, contact-person notifications, and calendar invitations. |
visitorFeatureTypeExtension |
Creates the visitor data type (name, email, company, contact person, check-in status fields). |
visitorEventsFeatureTypeExtension |
Creates the visitor events data type (sign-in / sign-out log). |
visitorFeatureBOMExtension |
Adds a visitors management page to the backoffice navigation. |
visitorEventsFeatureBOMExtension |
Adds a visitor events log page to the backoffice navigation. |
Server-Side API
The widget communicates with the following project-scoped API endpoints:
| Method | Path | Description |
|---|---|---|
POST |
/api/projects/[projectId]/visitors |
Creates a new visitor record. Body: { collectionName, data }. Returns { id } with status 201. |
GET |
/api/projects/[projectId]/visitors/[visitorId] |
Retrieves a visitor record. Query param: collectionName. |
PATCH |
/api/projects/[projectId]/visitors/[visitorId] |
Updates a visitor record. Body: { collectionName, data }. |
DELETE |
/api/projects/[projectId]/visitors/[visitorId] |
Deletes a visitor record. Query param: collectionName. |
POST |
/api/projects/[projectId]/visitors/[visitorId]/events |
Creates a visitor event (check-in or check-out). Body: { collectionName, data }. Returns { id } with status 201. |
Widget Registration
Widgets are registered in the central registry:
// Widget registration example
registerWidget({
name: WIDGET_TEXT,
title: { de: "Text" },
description: { en: "A simple text display widget for showing static or dynamic text content with configurable formatting and support for Text Replacer syntax." },
visibleInCatalog: true,
category: "content",
componentLoader: () => import("../../widgets/text/TextWidget.svelte"),
dataSchema: () => import("../../widgets/text/TextWidget.schema.json")
});
Registration Properties
- name: Unique widget identifier (constant)
- title: Localized display name
- description: Localized widget description as
InlineTranslationobject (required for catalog widgets) - category: Organization category for the widget picker
- visibleInCatalog: Whether to show in the visual editor
- componentLoader: Dynamic component import function
- dataSchema: Configuration schema for the widget editor
Description Requirement
Description is required for all widgets with visibleInCatalog: true. This ensures:
- AI agents and chatbots can accurately describe widget capabilities via
/docs/widgets/llm.txt - Users can discover widget features in the widget catalog with markdown rendering
- Developers have concise reference material during integration
The description should include:
- Overview of the widget's purpose
- Key features and capabilities
- Important configuration details
- Configuration properties table
- Use cases
- Technical implementation details (where relevant)
Accessing Widget Documentation
import {
getWidgetDescription,
getAllWidgetDocumentation
} from '@smallstack/client';
// Get description for a specific widget
const description = getWidgetDescription('EventStreamMap');
// Get all widget documentation (for documentation tools/chatbots)
const allDocs = await getAllWidgetDocumentation();
Widget documentation is also available via LLM-friendly endpoints:
- Main platform docs:
/llm.txt- Links to widget catalog - Widget catalog:
/docs/widgets/llm.txt- Complete widget documentation for AI agents
Widget Schemas
Each widget can define a JSON schema for its configuration:
{
"type": "object",
"properties": {
"text": {
"type": "string",
"title": "Text Content",
"description": "The text to display"
},
"fontSize": {
"type": "string",
"title": "Font Size",
"enum": ["small", "medium", "large"],
"default": "medium"
},
"color": {
"type": "string",
"title": "Text Color",
"x-type-schema": {
"inputWidgetConfiguration": {
"string": { "type": "color" }
}
}
}
}
}
Schema Features
- Type Validation: Ensures data integrity
- UI Generation: Automatically generates configuration forms
- Localization: Supports multiple languages
- Custom Input Types: Specialized input widgets
- Validation Rules: Client and server-side validation
Widget Lifecycle
sequenceDiagram
participant R as Registry
participant L as Loader
participant C as Component
participant E as Editor
R->>L: Register Widget
L->>L: Store Definition
E->>L: Request Widget
L->>C: Dynamic Import
C->>L: Component Instance
L->>E: Rendered Widget
E->>C: Configuration Updates
C->>C: Re-renderLifecycle Stages
- Registration: Widget is registered in the central registry
- Discovery: Widget becomes available in the widget picker
- Selection: User selects widget for their application
- Configuration: User configures widget properties
- Rendering: Widget is rendered in the application
- Updates: Configuration changes trigger re-rendering
- Persistence: Widget state is saved to the database
Widget Slots
Widgets can contain other widgets through a slot system:
interface WidgetSlotMapping {
[slotName: string]: string[]; // Maps slot names to widget IDs
}
Slot Examples
// Container widget with multiple slots
const containerWidget: Widget = {
id: "container-1",
name: "Container",
slots: {
"children": ["text-1", "image-1", "button-1"]
}
};
// App shell with named slots
const appShellWidget: Widget = {
id: "app-shell-1",
name: "AppShell",
slots: {
"header": ["navbar-1"],
"sidebar": ["navigation-1"],
"content": ["main-content-1"],
"footer": ["footer-1"]
}
};
Widget Styling
Widgets support CSS styling through the styles property:
interface WidgetCSSStyles {
[key: string]: string;
}
const styledWidget: Widget = {
id: "text-1",
name: "Text",
styles: {
"color": "#333333",
"fontSize": "18px",
"fontWeight": "bold",
"padding": "16px",
"margin": "8px 0"
}
};
Styling Features
- CSS Properties: Direct CSS property mapping
- Responsive Design: Media query support
- Theme Integration: Automatic theme variable usage
- Style Inheritance: Parent-child style relationships
Creating Custom Widgets
1. Create the Svelte Component
<!-- CustomWidget.svelte -->
<script lang="ts">
interface Props {
title?: string;
content?: string;
showIcon?: boolean;
}
const {
title = 'Default Title',
content = 'Default content',
showIcon = false
}: Props = $props();
</script>
<div class="custom-widget">
{#if showIcon}
<i class="fas fa-star"></i>
{/if}
<h3>{title}</h3>
<p>{content}</p>
</div>
2. Define the Schema
{
"type": "object",
"properties": {
"title": {
"type": "string",
"title": "Widget Title",
"default": "My Custom Widget"
},
"content": {
"type": "string",
"title": "Content",
"x-type-schema": {
"inputWidget": "textarea"
}
},
"showIcon": {
"type": "boolean",
"title": "Show Icon",
"default": false
}
}
}
3. Register the Widget
// In widget-registry.ts
registerWidget({
name: "CustomWidget",
title: { en: "Custom Widget", de: "Benutzerdefiniertes Widget" },
description: {
en: "A custom widget example",
de: "Ein Beispiel für ein benutzerdefiniertes Widget"
},
category: "custom",
visibleInCatalog: true,
componentLoader: () => import("./CustomWidget.svelte"),
dataSchema: () => import("./CustomWidget.schema.json")
});
Widget Testing
Unit Testing
import { render } from '@testing-library/svelte';
import { describe, it, expect } from 'vitest';
import CustomWidget from './CustomWidget.svelte';
describe('CustomWidget', () => {
it('renders with default props', () => {
const { getByText } = render(CustomWidget);
expect(getByText('Default Title')).toBeInTheDocument();
});
it('renders with custom data', () => {
const data = { title: 'Test Title', content: 'Test Content' };
const { getByText } = render(CustomWidget, { data });
expect(getByText('Test Title')).toBeInTheDocument();
expect(getByText('Test Content')).toBeInTheDocument();
});
});
Integration Testing
import { test, expect } from '@playwright/test';
test('widget can be added and configured', async ({ page }) => {
await page.goto('/editor');
// Add widget to page
await page.click('[data-testid="widget-picker"]');
await page.click('[data-testid="custom-widget"]');
// Configure widget
await page.fill('[data-testid="title-input"]', 'Test Title');
await page.fill('[data-testid="content-input"]', 'Test Content');
// Verify widget is rendered
await expect(page.locator('[data-testid="custom-widget"]')).toContainText('Test Title');
});
Best Practices
Widget Design
- Single Responsibility: Each widget should have one clear purpose
- Configurability: Expose relevant configuration options
- Accessibility: Follow WCAG guidelines for all interactive elements
- Performance: Optimize for fast loading and rendering
- Mobile-First: Design for mobile devices first
Schema Design
- Clear Naming: Use descriptive property names
- Validation: Include appropriate validation rules
- Defaults: Provide sensible default values
- Documentation: Include descriptions for all properties
- Localization: Support multiple languages where appropriate
Component Architecture
- Props Interface: Define clear TypeScript interfaces
- Reactive Updates: Use Svelte's reactivity system effectively
- Error Handling: Handle invalid props gracefully
- Style Isolation: Avoid global CSS conflicts
- Event Handling: Emit events for parent communication
Troubleshooting
Common Issues
- Widget Not Loading: Check dynamic import paths and component exports
- Schema Validation Errors: Verify JSON schema syntax and property types
- Styling Conflicts: Use CSS modules or scoped styles
- Performance Issues: Optimize component re-rendering and data loading
- Registration Errors: Ensure unique widget names and proper registration syntax
Debugging Tools
- Widget Inspector: Browser extension for widget tree visualization
- Schema Validator: Online tool for schema validation
- Component Profiler: Performance analysis for widget rendering
- Network Monitor: Track widget loading and data fetching
- Console Logging: Strategic logging for debugging widget behavior
Next Steps
- Learn about Widget Development
- Explore Widget Categories
- Understand Widget Lifecycle
- Review Widget Testing