Skip to content

Build Custom Web Components

Learn how to build custom web components that integrate seamlessly with Pika chat applications, supporting multiple rendering contexts and rich interactivity.

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
  • Node.js 22+ installed
  • TypeScript knowledge recommended
  • Familiarity with Web Components
  • Basic understanding of Svelte (if using Svelte)

Install the integration library:

Terminal window
pnpm install pika-shared
Section titled “Optional: Pika UX (Recommended for Svelte)”

For Svelte-based components with pre-built UI widgets:

Terminal window
pnpm install pika-ux

Benefits 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
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 element
customElements.define('acme-hello', HelloWidget);
<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>

The context object provides access to application and chat state.

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
}
const user = context.appState.identity.user;
console.log(user.userId, user.firstName, user.customData);
context.appState.showToast('Operation successful!', { type: 'success' });
const creds = await context.appState.identity.getUserAwsCredentials();
// Use for S3 uploads, etc.
// Current session
const session = context.chatAppState.currentSession;
// Messages
const 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;
}
context.chatAppState.chatInput = 'Show me the sales report';
await context.chatAppState.sendMessage();
await context.chatAppState.uploadFiles(selectedFiles);
const content = await context.chatAppState.getS3TextFileContent('config/widget-config.json');
const config = JSON.parse(content);

Components can render in different contexts with adapted UIs.

<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 detail view in dialog
await context.chatAppState.renderTag('acme.item-details', 'dialog', {
itemId: '12345'
});
// Open editor in canvas
await context.chatAppState.renderTag('acme.editor', 'canvas', {
documentId: 'doc-789'
});
// Add widget to spotlight
await 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
}
}
]
}
);
// Parent widget opens child with data
async function openProductDetails(productId: string) {
await context.chatAppState.renderTag('acme.product-details', 'dialog', {
productId: productId,
source: 'dashboard',
timestamp: Date.now()
});
}
// Child widget receives data
let productId = $state<string>('');
$effect(() => {
if (context?.dataForWidget) {
productId = context.dataForWidget.productId;
const source = context.dataForWidget.source;
const timestamp = context.dataForWidget.timestamp;
}
});

Widgets can provide interactive action buttons in their chrome (title bar). Action buttons automatically receive full context when clicked.

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' });
}
};
}

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()]
}
});
<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}

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 dynamically
metadata.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 action
metadata.addAction({
id: 'new-action',
title: 'New Action',
iconSvg: iconSvg,
callback: async () => { /* ... */ }
});
// Update action properties
metadata.updateAction('refresh', {
disabled: true,
title: 'Refreshing...'
});
// Remove action
metadata.removeAction('export');

Instead of creating database tag definitions, web components can register themselves in spotlight programmatically at runtime.

import { getPikaContext } from 'pika-shared/util/wc-utils';
// Inside your component's initialization
async 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)
});
}

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
});
}
}

Register a widget definition but don't show it automatically:

// Register the definition
context.chatAppState.manuallyRegisterSpotlightWidget({
tag: 'notification-center',
scope: 'acme',
tagTitle: 'Notifications',
autoCreateInstance: false // Don't show automatically
});
// Later, show it programmatically when needed
async function showNotifications() {
await context.chatAppState.renderTag('acme.notification-center', 'spotlight', {
unreadCount: 5
});
}

Combine registration with data passing:

// Register
context.chatAppState.manuallyRegisterSpotlightWidget({
tag: 'user-dashboard',
scope: 'acme',
tagTitle: 'User Dashboard',
autoCreateInstance: false
});
// Render with data
await context.chatAppState.renderTag('acme.user-dashboard', 'spotlight', {
userId: user.userId,
preferences: userPreferences,
theme: 'dark'
});
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;
}

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 programmatically
await context.chatAppState.saveSpotlightInstance(
'acme',
'saved-chart',
'Q4 Revenue Chart',
'acme-saved-chart',
{ chartData: {...} }
);

Components can directly invoke LLM agents.

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 request
2. Use the weather tool to fetch real-time data
3. 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}}`
}
};
<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>

Web components can easily convert markdown to HTML using the built-in helper method.

const context = await getPikaContext(this);
// Basic usage with default configuration
const html = context.appState.convertMarkdownToHtml('# Hello **World**');
// Output: <h1>Hello <strong>World</strong></h1>

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>
`;

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>
`;

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'
}
);

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)
});
<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>

Build beautiful UIs quickly with pre-built components.

<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>
<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>
<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>
dist/my-widget.js
# Using Vite
pnpm run build

See Deploy Custom Web Components for complete deployment instructions including:

  • Gzipping files
  • Uploading to S3
  • Creating tag definitions
  • Registering with Pika
  • 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
  • Minimize State: Keep component state minimal
  • Derive Values: Use $derived for computed values
  • Avoid Mutations: Use immutable updates
  • Clean Up: Remove event listeners in disconnectedCallback
  • Lazy Loading: Load components when needed
  • Debounce Input: Debounce user input handlers
  • Virtual Scrolling: For long lists
  • Optimize Renders: Only update what changed
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' });
}
}

Verify your component works correctly:

<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}
<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>
  • Verify custom element registered (customElements.define)
  • Check tag definition deployed correctly
  • Ensure component JavaScript loaded
  • Review browser console for errors
  • Ensure calling getPikaContext() after component connected
  • Check Pika context initialization
  • Verify component loaded in valid context
  • Review async/await usage
  • Check svelte.config.js configured for custom elements
  • Verify shadow: 'none' in component options
  • Ensure proper exports in entry file
  • Review Vite configuration