Skip to main content
Widgets are client components that render on the Ekanos dashboard. Use the Widget.* compound API for consistent styling and behavior.

Basic widget

Widget declaration

  • id — globally unique; prefix with your slug to avoid collisions
  • widgetState — required: 'active', 'inactive', or 'disabled'. id, name, component, and widgetState are the only required fields
  • layouts — seeds the default dashboard placement per breakpoint (lg, md, sm); gridSize and gridPosition are the older equivalents and still accepted
  • isPinnable — reaches WidgetContext and adds “Pin to board” / “Unpin from board” to the Widget.Header ⋯ menu
Fields the platform populates at runtime (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 is active, so inactive/disabled still win. An inactive prop forces the inactive state (for example, activated but the upstream connection is missing):
Three optional props opt the widget into the shared refresh and freshness chrome: With TanStack Query:
Pass none of them and the widget behaves as before. The host cannot see your queries, so each affordance appears only when you supply what it needs.

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.
Every 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’s onRefresh
  • Pin to board / Unpin from board — when the widget declares isPinnable
Add your own items with the 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 inside Widget.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 fetch clientReadable storage keys:
Keys without clientReadable: true are simply absent from the result, as are keys never written. See Storage.

Styling

Use semantic Tailwind classes (Ekanos design tokens):
Avoid 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 in WidgetPreviewProvider 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 loading you pass to Widget.DataState eventually becomes false
  • Your query’s enabled flag reads the integration state from WidgetContext outside Widget.DataState
  • fetchIntegrationStorage() keys have clientReadable: 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