Learn how to build custom web components that integrate seamlessly with Pika chat applications, supporting multiple rendering contexts and rich interactivity.
What You'll Accomplish
Section titled “What You'll Accomplish”By the end of this guide, you will:
- Create web components using vanilla JavaScript or Svelte
- Access Pika context and application state
- Build context-aware components (spotlight, canvas, dialog, inline)
- Implement tool use and agent invocation from components
- Use the pika-ux component library
- Handle user interactions and state management
Prerequisites
Section titled “Prerequisites”- Node.js 22+ installed
- TypeScript knowledge recommended
- Familiarity with Web Components
- Basic understanding of Svelte (if using Svelte)
Installation
Section titled “Installation”Required: Pika Shared
Section titled “Required: Pika Shared”Install the integration library:
pnpm install pika-sharedOptional: Pika UX (Recommended for Svelte)
Section titled “Optional: Pika UX (Recommended for Svelte)”For Svelte-based components with pre-built UI widgets:
pnpm install pika-uxBenefits of Svelte + pika-ux:
- CLI to create skeleton projects
- Pre-built UI components (buttons, dialogs, forms)
- Dramatically smaller compiled files
- Same components Pika uses internally
Step 1: Create Your First Component
Section titled “Step 1: Create Your First Component”Basic Vanilla JavaScript Component
Section titled “Basic Vanilla JavaScript Component”import { getPikaContext } from 'pika-shared/util/wc-utils';
class HelloWidget extends HTMLElement { connectedCallback() { this.init(); }
async init() { // Get Pika context const context = await getPikaContext(this);
// Access user information const user = context.appState.identity.user;
// Render content this.innerHTML = ` <div class="hello-widget"> <h3>Hello, ${user.firstName}!</h3> <p>You're in the ${context.context} context</p> <p>Chat App: ${context.chatAppId}</p> </div> `; }}
// Register the custom elementcustomElements.define('acme-hello', HelloWidget);Using Svelte (Recommended)
Section titled “Using Svelte (Recommended)”<svelte:options customElement={{ tag: 'acme-dashboard', shadow: 'none' }} />
<script lang="ts"> import { getPikaContext } from 'pika-shared/util/wc-utils'; import { type PikaWCContext } from 'pika-shared/types/chatbot/webcomp-types'; import { onMount } from 'svelte';
let context = $state<PikaWCContext>(); let initialized = $state(false);
$effect(() => { if (!initialized) { init(); } });
async function init() { context = await getPikaContext($host()); initialized = true; }</script>
{#if initialized && context} <div class="dashboard"> <h2>Sales Dashboard</h2> <p>User: {context.appState.identity.user.firstName}</p> <p>Context: {context.context}</p> </div>{:else} <p>Loading...</p>{/if}
<style> .dashboard { padding: 1rem; border: 1px solid #ddd; border-radius: 8px; }</style>Step 2: Access Pika Context
Section titled “Step 2: Access Pika Context”The context object provides access to application and chat state.
PikaWCContext Interface
Section titled “PikaWCContext Interface”interface PikaWCContext { appState: IAppState; // Global app state chatAppState: IChatAppState; // Chat-specific state renderingContext: WidgetRenderingContextType; // spotlight | inline | dialog | canvas chatAppId: string; instanceId: string; // Unique ID for this instance dataForWidget: Record<string, any>; // Data passed when opening widget}Access User Information
Section titled “Access User Information”const user = context.appState.identity.user;console.log(user.userId, user.firstName, user.customData);Show Toast Notifications
Section titled “Show Toast Notifications”context.appState.showToast('Operation successful!', { type: 'success' });Get AWS Credentials
Section titled “Get AWS Credentials”const creds = await context.appState.identity.getUserAwsCredentials();// Use for S3 uploads, etc.Access Chat State
Section titled “Access Chat State”// Current sessionconst session = context.chatAppState.currentSession;
// Messagesconst messages = context.chatAppState.currentSessionMessages;
// Custom data from auth provider (API keys, config)const customData = context.chatAppState.customDataForChatApp;if (customData) { const apiKey = customData.apiKey as string; const endpoint = customData.apiEndpoint as string;}Send Messages Programmatically
Section titled “Send Messages Programmatically”context.chatAppState.chatInput = 'Show me the sales report';await context.chatAppState.sendMessage();Upload Files
Section titled “Upload Files”await context.chatAppState.uploadFiles(selectedFiles);Retrieve S3 File Content
Section titled “Retrieve S3 File Content”const content = await context.chatAppState.getS3TextFileContent('config/widget-config.json');const config = JSON.parse(content);Step 3: Build Multi-Context Components
Section titled “Step 3: Build Multi-Context Components”Components can render in different contexts with adapted UIs.
Context-Aware Rendering
Section titled “Context-Aware Rendering”<script lang="ts"> import { getPikaContext } from 'pika-shared/util/wc-utils'; import { type PikaWCContext } from 'pika-shared/types/chatbot/webcomp-types';
let context = $state<PikaWCContext>();
async function init() { context = await getPikaContext($host()); }
$effect(() => { init(); });</script>
{#if context} {#if context.context === 'spotlight'} <!-- Compact view for spotlight --> <div class="compact"> <h4>Quick Stats</h4> <button onclick={() => context.chatAppState.renderTag('acme.dashboard', 'canvas')}> Open Full View </button> </div>
{:else if context.context === 'canvas'} <!-- Full view for canvas --> <div class="full-dashboard"> <h2>Complete Dashboard</h2> <!-- Rich content, charts, tables --> </div>
{:else if context.context === 'dialog'} <!-- Focused view for dialog --> <div class="dialog-content"> <h3>Quick Actions</h3> <!-- Form or settings --> </div>
{:else} <!-- Inline view --> <div class="inline-widget"> <!-- Summary or visualization --> </div> {/if}{/if}Open Other Widgets
Section titled “Open Other Widgets”// Open detail view in dialogawait context.chatAppState.renderTag('acme.item-details', 'dialog', { itemId: '12345'});
// Open editor in canvasawait context.chatAppState.renderTag('acme.editor', 'canvas', { documentId: 'doc-789'});
// Add widget to spotlightawait context.chatAppState.renderTag('acme.quick-actions', 'spotlight');
// Open widget with metadata (title, actions, icon)await context.chatAppState.renderTag('acme.dashboard', 'canvas', { userId: '123' }, { title: 'Sales Dashboard', lucideIconName: 'bar-chart', actions: [ { id: 'refresh', title: 'Refresh Data', iconSvg: refreshIconSvg, callback: async ({ context }) => { // Refresh logic } } ] });Pass Data Between Widgets
Section titled “Pass Data Between Widgets”// Parent widget opens child with dataasync function openProductDetails(productId: string) { await context.chatAppState.renderTag('acme.product-details', 'dialog', { productId: productId, source: 'dashboard', timestamp: Date.now() });}
// Child widget receives datalet productId = $state<string>('');
$effect(() => { if (context?.dataForWidget) { productId = context.dataForWidget.productId; const source = context.dataForWidget.source; const timestamp = context.dataForWidget.timestamp; }});Add Action Buttons to Widgets
Section titled “Add Action Buttons to Widgets”Widgets can provide interactive action buttons in their chrome (title bar). Action buttons automatically receive full context when clicked.
Basic Action Button
Section titled “Basic Action Button”import { extractIconSvg } from 'pika-shared/util/icon-utils';import type { WidgetAction, WidgetMetadata } from 'pika-shared/types/chatbot/webcomp-types';
async function createRefreshAction(): Promise<WidgetAction> { return { id: 'refresh', title: 'Refresh Data', iconSvg: await extractIconSvg('refresh-cw', 'lucide'), callback: async ({ element, instanceId, context }) => { // element: Direct access to widget DOM element // instanceId: Unique ID for this widget instance // context: Full PikaWCContext with app/chat state
const widget = element as MyWidget; await widget.refreshData(); context.appState.showToast('Data refreshed!', { type: 'success' }); } };}Registering Actions via Metadata
Section titled “Registering Actions via Metadata”Option 1: Pass metadata when rendering
const refreshAction = await createRefreshAction();
await context.chatAppState.renderTag( 'acme.dashboard', 'canvas', { userId: '123' }, { title: 'My Dashboard', lucideIconName: 'chart-line', actions: [refreshAction] });Option 2: Update metadata dynamically
async function init() { const context = await getPikaContext(this);
// Get metadata API const metadata = context.chatAppState.getWidgetMetadataAPI( 'acme', 'dashboard', context.instanceId, context.renderingContext );
// Set initial metadata with actions metadata.setMetadata({ title: 'Sales Dashboard', lucideIconName: 'bar-chart', actions: [ await createRefreshAction(), await createExportAction() ] });}Option 3: Include in SpotlightWidgetDefinition
context.chatAppState.manuallyRegisterSpotlightWidget({ tag: 'dashboard', scope: 'acme', tagTitle: 'Sales Dashboard', metadata: { title: 'Sales Dashboard', lucideIconName: 'chart-line', actions: [await createRefreshAction()] }});Complete Action Button Example
Section titled “Complete Action Button Example”<svelte:options customElement={{ tag: 'acme-widget', shadow: 'none' }} />
<script lang="ts"> import { getPikaContext } from 'pika-shared/util/wc-utils'; import { extractIconSvg } from 'pika-shared/util/icon-utils'; import type { PikaWCContext, WidgetAction } from 'pika-shared/types/chatbot/webcomp-types';
let context = $state<PikaWCContext>(); let data = $state<any[]>([]); let loading = $state(false);
async function init() { context = await getPikaContext($host());
// Define actions const refreshAction: WidgetAction = { id: 'refresh', title: 'Refresh', iconSvg: await extractIconSvg('refresh-cw', 'lucide'), callback: async ({ element, context: widgetContext }) => { const widget = element as any; await widget.refreshData(); } };
const exportAction: WidgetAction = { id: 'export', title: 'Export CSV', iconSvg: await extractIconSvg('download', 'lucide'), callback: async ({ element, context: widgetContext }) => { const widget = element as any; await widget.exportData(); } };
// Register metadata with actions const metadata = context.chatAppState.getWidgetMetadataAPI( 'acme', 'widget', context.instanceId, context.renderingContext );
metadata.setMetadata({ title: 'Data View', lucideIconName: 'table', actions: [refreshAction, exportAction] });
await loadData(); }
async function loadData() { loading = true; try { data = await fetchData(); } finally { loading = false; } }
// Exposed methods called by action buttons export async function refreshData() { await loadData(); context?.appState.showToast('Data refreshed', { type: 'success' }); }
export async function exportData() { const csv = convertToCSV(data); downloadFile(csv, 'export.csv'); context?.appState.showToast('Export complete', { type: 'success' }); }
$effect(() => { init(); });</script>
{#if loading} <p>Loading...</p>{:else} <div class="data-view"> {#each data as item} <div>{item.name}</div> {/each} </div>{/if}Action Button Features
Section titled “Action Button Features”Loading States
callback: async ({ instanceId, context }) => { const metadata = context.chatAppState.getWidgetMetadataAPI( 'acme', 'widget', instanceId, context.renderingContext );
metadata.setLoadingStatus(true, 'Processing...'); try { await processData(); } finally { metadata.setLoadingStatus(false); }}Disabled Actions
const action: WidgetAction = { id: 'save', title: 'Save', iconSvg: saveIconSvg, disabled: !hasChanges, // Disable when no changes callback: async ({ context }) => { await saveChanges(); }};
// Update disabled state dynamicallymetadata.updateAction('save', { disabled: false });Primary Actions (Dialog Footer)
const confirmAction: WidgetAction = { id: 'confirm', title: 'Confirm', iconSvg: checkIconSvg, primary: true, // Renders as prominent button in dialogs callback: async ({ context }) => { await handleConfirm(); context.chatAppState.closeDialog(); }};Dynamic Action Updates
// Add actionmetadata.addAction({ id: 'new-action', title: 'New Action', iconSvg: iconSvg, callback: async () => { /* ... */ }});
// Update action propertiesmetadata.updateAction('refresh', { disabled: true, title: 'Refreshing...'});
// Remove actionmetadata.removeAction('export');Register Spotlight Widgets Dynamically
Section titled “Register Spotlight Widgets Dynamically”Instead of creating database tag definitions, web components can register themselves in spotlight programmatically at runtime.
Basic Registration
Section titled “Basic Registration”import { getPikaContext } from 'pika-shared/util/wc-utils';
// Inside your component's initializationasync function init() { const context = await getPikaContext(this);
// Register this component in spotlight context.chatAppState.manuallyRegisterSpotlightWidget({ tag: 'my-widget', scope: 'my-app', tagTitle: 'My Widget', customElementName: 'my-app-my-widget', displayOrder: 0, // Optional: controls position autoCreateInstance: true, // Optional: show immediately (default: true) singleton: true // Optional: only one instance (default: true) });}Advanced: Conditional Registration
Section titled “Advanced: Conditional Registration”Register widgets only when certain conditions are met:
async function checkAndRegister() { const context = await getPikaContext(this); const user = context.appState.identity.user;
// Only register for admin users if (context.appState.identity.isSiteAdmin) { context.chatAppState.manuallyRegisterSpotlightWidget({ tag: 'admin-tools', scope: 'acme', tagTitle: 'Admin Tools', displayOrder: 0, autoCreateInstance: true }); }
// Or register based on feature flags const hasFeature = await checkFeatureFlag('beta-features'); if (hasFeature) { context.chatAppState.manuallyRegisterSpotlightWidget({ tag: 'beta-widget', scope: 'acme', tagTitle: 'Beta Features', displayOrder: 10 }); }}Control Auto-Display
Section titled “Control Auto-Display”Register a widget definition but don't show it automatically:
// Register the definitioncontext.chatAppState.manuallyRegisterSpotlightWidget({ tag: 'notification-center', scope: 'acme', tagTitle: 'Notifications', autoCreateInstance: false // Don't show automatically});
// Later, show it programmatically when neededasync function showNotifications() { await context.chatAppState.renderTag('acme.notification-center', 'spotlight', { unreadCount: 5 });}Pass Initial Data
Section titled “Pass Initial Data”Combine registration with data passing:
// Registercontext.chatAppState.manuallyRegisterSpotlightWidget({ tag: 'user-dashboard', scope: 'acme', tagTitle: 'User Dashboard', autoCreateInstance: false});
// Render with dataawait context.chatAppState.renderTag('acme.user-dashboard', 'spotlight', { userId: user.userId, preferences: userPreferences, theme: 'dark'});SpotlightWidgetDefinition Interface
Section titled “SpotlightWidgetDefinition Interface”interface SpotlightWidgetDefinition { /** Unique tag name (e.g., 'my-widget') */ tag: string;
/** Scope/namespace (e.g., 'my-app') */ scope: string;
/** Display title shown in UI */ tagTitle: string;
/** Custom element name (optional, inferred from tag if not provided) */ customElementName?: string;
/** Widget sizing configuration */ sizing?: WidgetSizing;
/** Agent instructions for component invocation */ componentAgentInstructionsMd?: Record<string, string>;
/** Auto-show widget? Default: true */ autoCreateInstance?: boolean;
/** Display order in spotlight (lower = higher). Default: 0 */ displayOrder?: number;
/** Only allow one instance? Default: true */ singleton?: boolean;
/** Show in unpinned menu? Default: true */ showInUnpinnedMenu?: boolean;
/** * Optional initial metadata (title, actions, icon). * Applied when widget is rendered. * @since 0.11.0 */ metadata?: WidgetMetadata;}Common Patterns
Section titled “Common Patterns”Development Mode Registration:
if (import.meta.env.DEV) { context.chatAppState.manuallyRegisterSpotlightWidget({ tag: 'dev-tools', scope: 'internal', tagTitle: 'Dev Tools', displayOrder: 0 });}Feature-Gated Widget:
const features = await context.chatAppState.getEnabledFeatures();if (features.includes('advanced-analytics')) { context.chatAppState.manuallyRegisterSpotlightWidget({ tag: 'analytics-dashboard', scope: 'acme', tagTitle: 'Analytics', displayOrder: 5 });}Base Widget for Saved Instances (Virtual Tags Pattern):
// Register base definition (doesn't show by default)context.chatAppState.manuallyRegisterSpotlightWidget({ tag: 'saved-chart', scope: 'acme', tagTitle: 'Saved Chart', autoCreateInstance: false, // Don't create a default instance singleton: false, // Allow multiple instances (KEY: enables saved instances) showInUnpinnedMenu: false // Don't show in menu (use save flow instead)});
// Later, create saved instances programmaticallyawait context.chatAppState.saveSpotlightInstance( 'acme', 'saved-chart', 'Q4 Revenue Chart', 'acme-saved-chart', { chartData: {...} });Step 4: Invoke Agents from Components
Section titled “Step 4: Invoke Agents from Components”Components can directly invoke LLM agents.
Define Component Instructions
Section titled “Define Component Instructions”When creating your tag definition, include component agent instructions:
const weatherWidgetTag = { tag: 'weather-dashboard', scope: 'acme', // ... other fields componentAgentInstructionsMd: { 'getCurrentWeather': `You are a weather data assistant. When invoked:
1. Extract the location(s) from the user's request2. Use the weather tool to fetch real-time data3. Return structured weather information
<output_schema>interface WeatherDataResponse { locations: WeatherData[];}
interface WeatherData { location: string; tempF: number; tempC: number; condition: string; timestamp: string;}</output_schema>
{{typescript-backed-output-formatting-requirements}}` }};Invoke Agent from Component
Section titled “Invoke Agent from Component”<script lang="ts"> import { getPikaContext } from 'pika-shared/util/wc-utils'; import { Button } from 'pika-ux/shadcn/button';
let context = $state<PikaWCContext>(); let weatherData = $state<any>(null); let loading = $state(false);
async function getWeather(location: string) { loading = true; try { const response = await context.chatAppState.invokeAgentAsComponent( 'acme', // scope 'weather-dashboard', // tag 'getCurrentWeather', // instruction name `Get weather for ${location}` ); weatherData = response; } catch (error) { context.appState.showToast('Failed to get weather', { type: 'error' }); } finally { loading = false; } }</script>
<div> <Button onclick={() => getWeather('San Francisco')} disabled={loading}> Get Weather </Button>
{#if weatherData} <div class="weather-results"> {#each weatherData.locations as location} <div class="weather-card"> <h4>{location.location}</h4> <p>Temperature: {location.tempF}°F ({location.tempC}°C)</p> <p>Condition: {location.condition}</p> </div> {/each} </div> {/if}</div>Step 5: Convert Markdown to HTML
Section titled “Step 5: Convert Markdown to HTML”Web components can easily convert markdown to HTML using the built-in helper method.
Using convertMarkdownToHtml
Section titled “Using convertMarkdownToHtml”const context = await getPikaContext(this);
// Basic usage with default configurationconst html = context.appState.convertMarkdownToHtml('# Hello **World**');
// Output: <h1>Hello <strong>World</strong></h1>Styling the HTML Output
Section titled “Styling the HTML Output”The converted HTML needs CSS to display properly. Choose one of these options:
Option 1: Tailwind Typography CDN (Easiest)
Section titled “Option 1: Tailwind Typography CDN (Easiest)”Add this to your component's HTML or your application's index.html:
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@tailwindcss/typography@0.5.15/dist/typography.min.css">Then wrap your HTML content:
this.innerHTML = ` <div class="prose prose-gray max-w-none"> ${html} </div>`;Option 2: Custom CSS (No Dependencies)
Section titled “Option 2: Custom CSS (No Dependencies)”If you prefer not to use Tailwind, include minimal custom CSS:
/* Add to your component's styles */.markdown-content { word-break: break-word;}
.markdown-content pre { border-radius: 0.375rem; padding: 1rem; overflow-x: auto; background-color: #f3f4f6;}
.markdown-content code { background-color: #f3f4f6; border-radius: 0.25rem; padding: 0.125rem 0.25rem; font-size: 0.875rem;}
.markdown-content blockquote { border-left: 4px solid #d1d5db; padding-left: 1rem; font-style: italic;}
.markdown-content table { width: 100%; border-collapse: collapse;}
.markdown-content th,.markdown-content td { border: 1px solid #d1d5db; padding: 0.5rem 0.75rem;}
.markdown-content th { background-color: #f3f4f6; font-weight: 600;}Then use the class:
this.innerHTML = ` <div class="markdown-content"> ${html} </div>`;With Syntax Highlighting
Section titled “With Syntax Highlighting”For code syntax highlighting, provide a custom highlight function:
import hljs from 'highlight.js';import 'highlight.js/styles/github-dark.css';
const html = context.appState.convertMarkdownToHtml( '```javascript\nconst x = 42;\nconsole.log(x);\n```', { highlight: (str: string, lang: string): string => { if (lang && hljs.getLanguage(lang)) { try { return ( '<pre class="hljs"><code>' + hljs.highlight(str, { language: lang, ignoreIllegals: true }).value + '</code></pre>' ); } catch (__) {} } return '<pre class="hljs"><code>' + str + '</code></pre>'; }, highlightCacheKey: 'hljs-github-dark' });Custom Configuration
Section titled “Custom Configuration”Customize markdown rendering behavior:
const html = context.appState.convertMarkdownToHtml(markdown, { html: true, // Allow HTML tags in source (default: true) linkify: true, // Auto-convert URLs to links (default: true) typographer: true, // Smart quotes and typography (default: true) breaks: false // Don't convert \n to <br> (default: true)});Complete Svelte Example
Section titled “Complete Svelte Example”<svelte:options customElement={{ tag: 'acme-markdown-viewer', shadow: 'none' }} />
<script lang="ts"> import { getPikaContext } from 'pika-shared/util/wc-utils'; import type { PikaWCContext } from 'pika-shared/types/chatbot/webcomp-types';
let context = $state<PikaWCContext>(); let markdownInput = $state('# Hello World\n\nThis is **bold** text.'); let htmlOutput = $state('');
async function init() { context = await getPikaContext($host()); }
function convertMarkdown() { if (context) { htmlOutput = context.appState.convertMarkdownToHtml(markdownInput); } }
$effect(() => { init(); });</script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@tailwindcss/typography@0.5.15/dist/typography.min.css">
<div class="markdown-viewer"> <h3>Markdown Input:</h3> <textarea bind:value={markdownInput} rows="10"></textarea>
<button onclick={convertMarkdown}>Convert</button>
<h3>HTML Output:</h3> <div class="prose prose-gray max-w-none"> {@html htmlOutput} </div></div>
<style> .markdown-viewer { padding: 1rem; }
textarea { width: 100%; padding: 0.5rem; font-family: monospace; }
button { margin: 1rem 0; padding: 0.5rem 1rem; background: #3b82f6; color: white; border-radius: 0.25rem; cursor: pointer; }</style>Step 6: Use Pika UX Components
Section titled “Step 6: Use Pika UX Components”Build beautiful UIs quickly with pre-built components.
Button Examples
Section titled “Button Examples”<script lang="ts"> import { Button } from 'pika-ux/shadcn/button';</script>
<Button onclick={handleClick} variant="default" size="lg"> Click Me</Button>
<Button variant="outline">Secondary</Button><Button variant="destructive">Delete</Button>Dialog Example
Section titled “Dialog Example”<script lang="ts"> import * as Dialog from 'pika-ux/shadcn/dialog'; import { Button } from 'pika-ux/shadcn/button';
let dialogOpen = $state(false);</script>
<Button onclick={() => dialogOpen = true}>Open Dialog</Button>
<Dialog.Root bind:open={dialogOpen}> <Dialog.Content> <Dialog.Header> <Dialog.Title>Confirm Action</Dialog.Title> <Dialog.Description> Are you sure you want to proceed? </Dialog.Description> </Dialog.Header>
<Dialog.Footer> <Button variant="outline" onclick={() => dialogOpen = false}> Cancel </Button> <Button onclick={handleConfirm}>Confirm</Button> </Dialog.Footer> </Dialog.Content></Dialog.Root>Form Components
Section titled “Form Components”<script lang="ts"> import { Input } from 'pika-ux/shadcn/input'; import { Label } from 'pika-ux/shadcn/label'; import { Button } from 'pika-ux/shadcn/button';
let email = $state(''); let message = $state('');</script>
<div class="space-y-4"> <div> <Label for="email">Email</Label> <Input id="email" type="email" placeholder="you@example.com" bind:value={email} /> </div>
<div> <Label for="message">Message</Label> <Input id="message" placeholder="Your message" bind:value={message} /> </div>
<Button onclick={handleSubmit}>Submit</Button></div>Step 6: Build and Deploy
Section titled “Step 6: Build and Deploy”Build Your Component
Section titled “Build Your Component”# Using Vitepnpm run buildDeploy to Pika
Section titled “Deploy to Pika”See Deploy Custom Web Components for complete deployment instructions including:
- Gzipping files
- Uploading to S3
- Creating tag definitions
- Registering with Pika
Best Practices
Section titled “Best Practices”Component Design
Section titled “Component Design”- Single Responsibility: Each component does one thing well
- Context Awareness: Adapt UI to rendering context
- Progressive Enhancement: Work without JavaScript as baseline
- Accessibility: Use semantic HTML and ARIA attributes
State Management
Section titled “State Management”- Minimize State: Keep component state minimal
- Derive Values: Use
$derivedfor computed values - Avoid Mutations: Use immutable updates
- Clean Up: Remove event listeners in disconnectedCallback
Performance
Section titled “Performance”- Lazy Loading: Load components when needed
- Debounce Input: Debounce user input handlers
- Virtual Scrolling: For long lists
- Optimize Renders: Only update what changed
Error Handling
Section titled “Error Handling”async function loadData() { try { const data = await fetchData(); processData(data); } catch (error) { console.error('Failed to load data:', error); context.appState.showToast('Failed to load data', { type: 'error' }); }}Testing Checklist
Section titled “Testing Checklist”Verify your component works correctly:
Common Patterns
Section titled “Common Patterns”Loading States
Section titled “Loading States”<script lang="ts"> let loading = $state(true); let data = $state(null);
async function loadData() { loading = true; try { data = await fetchData(); } finally { loading = false; } }
onMount(loadData);</script>
{#if loading} <p>Loading...</p>{:else if data} <DataDisplay {data} />{:else} <p>No data available</p>{/if}Confirmation Dialogs
Section titled “Confirmation Dialogs”<script lang="ts"> import * as AlertDialog from 'pika-ux/shadcn/alert-dialog';
let confirmOpen = $state(false);
async function handleDelete() { await deleteItem(); confirmOpen = false; context.appState.showToast('Item deleted', { type: 'success' }); }</script>
<Button variant="destructive" onclick={() => confirmOpen = true}> Delete</Button>
<AlertDialog.Root bind:open={confirmOpen}> <AlertDialog.Content> <AlertDialog.Header> <AlertDialog.Title>Are you sure?</AlertDialog.Title> <AlertDialog.Description> This action cannot be undone. </AlertDialog.Description> </AlertDialog.Header> <AlertDialog.Footer> <AlertDialog.Cancel>Cancel</AlertDialog.Cancel> <AlertDialog.Action onclick={handleDelete}>Delete</AlertDialog.Action> </AlertDialog.Footer> </AlertDialog.Content></AlertDialog.Root>Troubleshooting
Section titled “Troubleshooting”Component Not Rendering
Section titled “Component Not Rendering”- Verify custom element registered (
customElements.define) - Check tag definition deployed correctly
- Ensure component JavaScript loaded
- Review browser console for errors
Context Not Available
Section titled “Context Not Available”- Ensure calling
getPikaContext()after component connected - Check Pika context initialization
- Verify component loaded in valid context
- Review async/await usage
Svelte Build Issues
Section titled “Svelte Build Issues”- Check
svelte.config.jsconfigured for custom elements - Verify
shadow: 'none'in component options - Ensure proper exports in entry file
- Review Vite configuration
Next Steps
Section titled “Next Steps”- Deploy Custom Web Components - Deploy to production
- Work with Pika UX Module - Use pre-built components
- Custom Widget Tag Definitions - Create tag definitions
Related Documentation
Section titled “Related Documentation”- Web Components Capability - Learn about widget system
- Pika UX Components - Component library
- Tag Definition Guide - Tag management