Skip to content

Widget Development

Widgets are React components that render on token pages. They receive structured data via props and never call APIs directly.

Architecture

  • Rendered directly in the host page React tree (not iframes)
  • Receive all data via props, no direct API calls
  • SHA-256 integrity verification before execution
  • Sandboxed globals (eval, Function, document.cookie blocked)
  • Scoped localStorage (prefixed per widget ID)
  • DOM containment via MutationObserver

Widget Props

Every widget receives AppWidgetProps:

typescript
interface AppWidgetProps<TSettings = Record<string, unknown>> {
  appData: AppDataContext;        // Token data for the current page
  settings: TSettings;            // Widget-specific config
  widgetId: string;               // Your widget ID
  isOwner: boolean;               // Whether the viewer owns this token
  connectedAddress: string | null; // Viewer's wallet address
  chainKey: string;               // Chain key (e.g., "arb")
  theme: AppWidgetTheme;          // Host page colors
  onAction: (action) => void;     // Dispatch actions to the host
  breakpoint?: 'compact' | 'standard' | 'wide';
}

Token Data

The appData object provides structured token data:

typescript
interface AppDataContext {
  symbol: string;              // "DOGE"
  tokenName: string | null;
  description: string | null;
  tags: string[];
  chainKey: string;            // "arb"
  chainId: number | string;
  chainName: string;           // "Arbitrum One"
  tokenAddress: string | null;
  owner: string | null;        // Token owner's wallet address
  status: string;              // "active", "at-risk", "inactive"
  tokenId: number | null;
  market: {
    price: number | null;
    volume24h: number | null;
    marketCap: number | null;
    holders: number | null;
    totalSupply: number | null;
    circulatingSupply: number | null;
    liquidity?: number | null;
    txns24h?: number | null;
    priceChange1h?: number | null;
    priceChange6h?: number | null;
    priceChange24h?: number | null;
    sparkline7d?: number[] | null;
  };
  socialLinks: {
    website: string | null;
    xdotcom: string | null;
    telegram: string | null;
    discord: string | null;
    github: string | null;
    whitepaper: string | null;
  };
  verifications: { ... };      // Per-platform verification records
  metadata: Record<string, unknown>;
}

The full type definition ships with the SDK — import it from @chaindaddy/apps/types/widget in a project scaffolded by chaindaddy app init widget.

Theme

Match the host page's styling using the theme object:

typescript
interface AppWidgetTheme {
  mode: 'light' | 'dark';
  accentColor: string;
  backgroundColor: string;
  cardBackground: string;
  textPrimary: string;
  textSecondary: string;
  textMuted: string;
  borderColor: string;
}

Actions

Dispatch actions to the host platform via onAction():

typescript
// Navigate within the platform (token pages live at /{symbol}/{chain})
onAction({ type: 'navigate', path: '/doge/arbitrum' });

// Copy text to clipboard
onAction({ type: 'clipboard', text: 'DOGE' });

// Fire an analytics event
onAction({ type: 'track', event: 'widget_clicked', data: { section: 'price' } });

// Show a toast notification
onAction({ type: 'toast', message: 'Copied!', variant: 'success' });

// Open a platform modal
onAction({ type: 'modal', modal: 'holders', data: { symbol: 'DOGE' } });

The full action union (AppWidgetAction) also covers api-call, connect-wallet, sign-typed-data, transaction, and quote — each shape is declared in @chaindaddy/apps/types/widget, bundled with the chaindaddy app init widget scaffold.

Responsive Design

Widgets receive a breakpoint prop based on their container width:

BreakpointDescription
compactNarrow container (sidebar, mobile)
standardDefault width
wideFull-width container

Adapt your layout based on the breakpoint:

tsx
export function MyWidget({ appData, breakpoint }: AppWidgetProps) {
  if (breakpoint === 'compact') {
    return <CompactView data={appData} />;
  }
  return <StandardView data={appData} />;
}

Configurable Settings

Define a configSchema in your manifest to let token owners customize your widget:

json
{
  "configSchema": {
    "type": "object",
    "properties": {
      "showVolume": {
        "type": "boolean",
        "description": "Show 24h trading volume",
        "default": true
      },
      "alertPrice": {
        "type": "number",
        "description": "Price alert threshold"
      }
    }
  }
}

Access settings via the settings prop:

tsx
export function MyWidget({ appData, settings }: AppWidgetProps<MySettings>) {
  return (
    <div>
      <p>Price: ${appData.market.price}</p>
      {settings.showVolume && <p>Volume: ${appData.market.volume24h}</p>}
    </div>
  );
}

Lifecycle Hooks

Widgets can optionally implement lifecycle hooks:

typescript
interface AppWidgetLifecycleHooks {
  onWidgetMount?(context): void | Promise<void>;
  onWidgetUnmount?(context): void;
  onDataUpdate?(prevData, nextData): void;
  onSettingsChange?(prevSettings, nextSettings): void;
  onVisibilityChange?(visible: boolean): void;
}

The lifecycle context (WidgetLifecycleContext) provides widgetId, appData, scopedStorage, and an abortController for async cleanup.

SSR Compatibility

Widgets can export a renderStatic() function for server-side rendering:

typescript
export function renderStatic(appData: AppDataContext): string {
  return `<dl>
    <dt>Price</dt><dd>${appData.market.price ?? 'N/A'}</dd>
    <dt>Volume</dt><dd>${appData.market.volume24h ?? 'N/A'}</dd>
  </dl>`;
}

Building Your Widget

Widgets must compile to a single JavaScript bundle:

  1. Under 500 KB
  2. Export the component matching entryComponent as a named export
  3. Do not import React, it's provided by the host as an external
  4. Do not use eval(), new Function(), or access document.cookie

Example esbuild config:

javascript
import { build } from 'esbuild';

await build({
  entryPoints: ['src/index.tsx'],
  bundle: true,
  format: 'esm',
  outfile: 'dist/bundle.js',
  external: ['react', 'react-dom', 'react/jsx-runtime'],
  minify: true,
});

Minimal Example

tsx
import type { AppWidgetProps } from '@chaindaddy/apps/types/widget';

interface MySettings {
  showVolume?: boolean;
}

export function MyWidget({
  appData,
  settings,
  theme,
  onAction,
}: AppWidgetProps<MySettings>) {
  return (
    <div style={{ background: theme.cardBackground, color: theme.textPrimary }}>
      <h3>{appData.symbol}</h3>
      <p>Price: ${appData.market.price?.toFixed(6) ?? 'N/A'}</p>
      {settings.showVolume && (
        <p>Volume: ${appData.market.volume24h?.toLocaleString() ?? 'N/A'}</p>
      )}
      <button onClick={() => onAction({
        type: 'clipboard',
        text: appData.symbol
      })}>
        Copy Symbol
      </button>
    </div>
  );
}

Security Model

LayerEnforcement
Bundle integritySHA-256 hash verified before execution
Bundle sizeRejected if > 500 KB
Sandboxed globalseval, Function, document.cookie, sessionStorage blocked
Scoped storagelocalStorage keys prefixed with widget ID
Fetch restrictionOnly /api/ paths with fetch:self-api permission
DOM containmentModifications outside widget container are prevented
CSPScript sources locked to known CDN domains

White-Label Embedding

Partner tier and above can embed widgets without Chain Daddy branding. White-label mode removes the "Powered by Chain Daddy" footer and platform attribution from all embedded widgets and overlays.

To enable white-label mode, set branding: false in your widget configuration:

typescript
{
  widgetId: 'your-widget-id',
  branding: false,  // Partner tier required
}

Custom Branding

Developer tier and above can customize widget branding with their own colors, logo, and call-to-action text. Custom branding overrides the default Chain Daddy styling while keeping the widget functional.

Configure branding in your widget settings:

typescript
{
  branding: {
    logo: 'https://your-domain.com/logo.png',
    primaryColor: '#FF6B00',
    ctaText: 'Buy on YourDEX',
  }
}