Widget.* compound API for consistent styling and behavior.
Basic widget
Widget declaration
id— globally unique; prefix with your slug to avoid collisionswidgetState— required:'active','inactive', or'disabled'.id,name,component, andwidgetStateare the only required fieldslayouts— seeds the default dashboard placement per breakpoint (lg,md,sm);gridSizeandgridPositionare the older equivalents and still acceptedisPinnable— reachesWidgetContextand adds “Pin to board” / “Unpin from board” to theWidget.Header⋯ menu
productId, widgetConfigId, workspaceId, collapsed, isPinned, health, integrationMetadata) are rejected in a declaration.
Widget API
Widget.DataState
Overrides the render state with your widget’s own data state — only while the integration itself isactive, so inactive/disabled still win. An inactive prop forces the inactive state (for example, activated but the upstream connection is missing):
With TanStack Query:
Widget.Card
The card container (widget elevation shadow, no border):Widget.Header
Inset header.title, subtitle, and description (shown as a tooltip) override the provider’s values; children render on the right. It renders nothing when there is no title from either source.
Widget.Header also renders a ⋯ menu with the actions that apply. With none of them applicable, the menu does not render:
- Open in workspace — when the host sets
WidgetMeta.openHref - Refresh — when the widget reports a refresh through
Widget.DataState’sonRefresh - Pin to board / Unpin from board — when the widget declares
isPinnable
menu prop and Widget.MenuItem. They sit at the top of the menu. Use them for occasional actions such as “Open in QuickBooks”, and keep children for controls people use on every visit, like filters, so the header does not carry a row of buttons plus the menu.
Widget.MenuItem takes href (a link; external opens it in a new tab), onSelect, disabled, and icon (a Font Awesome class such as fa-light fa-envelope).
Widget.Active
Content area. By default,p-0 for full-bleed content. Wrap text in Widget.Body for standard padding:
Widget.Body
Padded flex-column container (px-5 pt-4 pb-5):
Other parts
Widget.Inactive and Widget.Disabled render for those states (with default content if you pass no children), Widget.Footer renders only while active, Widget.Actions is a muted footer bar, Widget.AskFooter renders the “Ask about this” bar, and Widget.HealthFooter shows a stale-data notice when the integration is degraded, including when it last synced (from WidgetHealth.lastSuccessAt) when that is known.
Every Widget.* part must render inside a widget provider. Outside one it throws Widget.* compound components must be rendered inside a widget provider. — the host supplies one on the dashboard; elsewhere use WidgetPreviewProvider.
Recovering from errors
The error boundary insideWidget.Active catches a crashing widget and offers a “Try again” button that re-mounts it (and reloads its data when the widget reports a refresh), instead of telling the user to reload the page. The default Widget.Error view shows the same “Try again” button when you pass onRefresh to Widget.DataState.
WidgetContext
Access widget metadata and state:WidgetContextValue also has an optional data field (WidgetData: updatedAt, refreshing, refresh), filled from Widget.DataState’s props, and ctx.meta.openHref. Both types are exported from @ekanos/sdk/components. Custom providers that build a WidgetContextValue keep compiling unchanged.
Read ctx?.state.state outside Widget.DataState to gate data fetching on the integration being active. ctx.meta carries widgetId, title, isCollapsible, isPinnable, aiFooterEnabled, and related display fields.
Reading storage
Widgets fetchclientReadable storage keys:
clientReadable: true are simply absent from the result, as are keys never written. See Storage.
Styling
Use semantic Tailwind classes (Ekanos design tokens):bg-white, text-black, or hex colors. See Styling for loading the token layer outside Ekanos.
Common patterns
Chart widget
Data table widget
Testing widgets
Wrap inWidgetPreviewProvider to render without the host. It fills the same WidgetContext with local state and no persistence; state defaults to 'active':
Troubleshooting
Widget ID collision
Widget IDs are globally unique. Prefix with your slug:acme-payments-summary, not just summary.
Widget always loading
Check:- The
loadingyou pass toWidget.DataStateeventually becomesfalse - Your query’s enabled flag reads the integration state from
WidgetContextoutsideWidget.DataState fetchIntegrationStorage()keys haveclientReadable: true(absent keys come back as missing, not as an error)
Styling looks wrong
Outside Ekanos, load the@ekanos/ui token layer — see Styling. Use semantic classes (bg-card, text-foreground), not hardcoded colors.
Next steps
- Marketplace tile — card for the integration marketplace
- Activation form — connect form
- Testing — test widgets