Appearance
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.cookieblocked) - 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:
| Breakpoint | Description |
|---|---|
compact | Narrow container (sidebar, mobile) |
standard | Default width |
wide | Full-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:
- Under 500 KB
- Export the component matching
entryComponentas a named export - Do not import React, it's provided by the host as an external
- Do not use
eval(),new Function(), or accessdocument.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
| Layer | Enforcement |
|---|---|
| Bundle integrity | SHA-256 hash verified before execution |
| Bundle size | Rejected if > 500 KB |
| Sandboxed globals | eval, Function, document.cookie, sessionStorage blocked |
| Scoped storage | localStorage keys prefixed with widget ID |
| Fetch restriction | Only /api/ paths with fetch:self-api permission |
| DOM containment | Modifications outside widget container are prevented |
| CSP | Script 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',
}
}