Compound components

Compound components let you assemble the same flow the drop-in dialogs render, but in your layout. The FSM, the gateway calls, the settlement watcher, the design tokens — all reused. The only thing you supply is the composition.

import { AssetPicker } from "@stridge/gateway-kit/deposit/widgets"

<AssetPicker />  // zero-prop. Reads driver state, dispatches FSM actions internally.

That's the default for a configured flow. Deposit and Withdraw widgets read their own controllers and drivers. Orchestrated Activity widgets also need the Activity controller and resolved owner described in Provider; provider ancestry alone does not create them. Bare Activity compounds use host-supplied data instead.

Two layers: widgets and parts

The compound tier ships two layers of API, both public, both stable:

LayerSubpathWhat it isWhen to reach for it
Widgets (default)@stridge/gateway-kit/{flow}/widgetsZero-prop orchestrated wrappers. Read the driver snapshot, dispatch FSM actions, render the canonical sub-part composition. Sub-parts aliased on the widget's namespace for custom layouts.Almost always — this is the layer the shadcn registry scaffolds against.
Parts@stridge/gateway-kit/{flow}/compoundBare compound parts plus flow-specific FSM routing. Activity also exports ActivitySourceProvider for host-owned history. The bare parts don't read a Kit driver on their own.FSM routing for a controller-backed flow, or a custom Activity view with host-supplied data.

The widget and the bare part of the same name (e.g. AssetPicker from /widgets and AssetPicker from /compound) share identity-equal sub-part references — <AssetPicker.Header /> from either subpath is the same component. The difference is what the root function does: the widget wires snapshot + actions; the bare part doesn't.

Note

The flow routing namespace lives on /{flow}/compound: Deposit.Boundary, .Guards, .Steps, .Step; the Withdraw mirrors; and ActivityFlow.Boundary, .Steps, .Step without Guards. ActivityFlowBoundary is also exported separately from /activity/dialog. Widgets sit inside a <Step>; they don't replace it.

The canonical composition

This is what the shadcn registry drops into your repo. Production deposit flows compose like this — FSM scaffolding from /compound for the routing, widgets from /widgets as the leaf children:

components/stridge/deposit/deposit.tsx
"use client"

import { useDeposit, useDepositState } from "@stridge/gateway-kit"
import { Deposit } from "@stridge/gateway-kit/deposit/compound" // FSM scaffolding
import { Dialog } from "@stridge/gateway-kit/ui"

import { StridgeDepositMethods } from "./methods"
import { StridgeDepositAssetPicker } from "./asset-picker"
import { StridgeDepositAmountEntry } from "./amount-entry"
import { StridgeDepositConfirm } from "./confirm"
import { StridgeDepositTransferCrypto } from "./transfer-crypto"
import { StridgeDepositProcessing } from "./processing"
import { StridgeDepositSuccess } from "./success"
import { StridgeDepositError } from "./error"
import { StridgeDepositActivityList } from "./activity-list"
import { StridgeDepositActivityDetail } from "./activity-detail"
import { StridgeDepositStatusBanner } from "./status-banner"

export function StridgeDeposit() {
  const state = useDepositState()
  const { close } = useDeposit()
  return (
    <Dialog open={state.name !== "closed"} onOpenChange={(open) => { if (!open) close() }}>
      <Dialog.Content>
        <Deposit.Boundary>
          <Deposit.Guards>
            <Deposit.Steps>
              <Deposit.Step name="deposit"><StridgeDepositMethods /></Deposit.Step>
              <Deposit.Step name="assetPicker"><StridgeDepositAssetPicker /></Deposit.Step>
              <Deposit.Step name="amountEntry"><StridgeDepositAmountEntry /></Deposit.Step>
              <Deposit.Step name="confirmDeposit"><StridgeDepositConfirm /></Deposit.Step>
              <Deposit.Step name="transferCrypto"><StridgeDepositTransferCrypto /></Deposit.Step>
              <Deposit.Step name="processing"><StridgeDepositProcessing /></Deposit.Step>
              <Deposit.Step name="success"><StridgeDepositSuccess /></Deposit.Step>
              <Deposit.Step name="error"><StridgeDepositError /></Deposit.Step>
              <Deposit.Step name="activityList"><StridgeDepositActivityList /></Deposit.Step>
              <Deposit.Step name="activityDetail"><StridgeDepositActivityDetail /></Deposit.Step>
            </Deposit.Steps>
          </Deposit.Guards>
          <StridgeDepositStatusBanner />
        </Deposit.Boundary>
      </Dialog.Content>
    </Dialog>
  )
}

Each leaf wrapper (StridgeDepositAssetPicker, StridgeDepositAmountEntry, …) is a thin file that mounts the matching widget with whatever composition you want:

components/stridge/deposit/asset-picker.tsx
"use client"

import { AssetPicker } from "@stridge/gateway-kit/deposit/widgets"

export function StridgeDepositAssetPicker() {
  return (
    <AssetPicker>
      <AssetPicker.Header />
      <AssetPicker.Body>
        <AssetPicker.List />
      </AssetPicker.Body>
      <AssetPicker.Footer />
    </AssetPicker>
  )
}

The widget reads the source-asset list from the driver and feeds it into <AssetPicker.List /> — your wrapper file just states the layout.

Custom compositions inside a widget

Every widget accepts children to swap the default composition. Pass them to interleave your own JSX between sub-parts, drop sub-parts you don't want, or wrap them.

Interleaving your own JSX

A worked example — adding a small explainer between the recipient and the amount fields on the withdraw form:

import { WithdrawForm } from "@stridge/gateway-kit/withdraw/widgets"

<WithdrawForm>
  <WithdrawForm.Header />
  <WithdrawForm.Body>
    <WithdrawForm.RecipientField />

    {/* Your custom component, slotted into the form. */}
    <YourRecipientHint />

    <WithdrawForm.AmountField />
    <WithdrawForm.ReceiveSelectors />
    <WithdrawForm.BreakdownCard />
  </WithdrawForm.Body>
  <WithdrawForm.Footer />
</WithdrawForm>

You can do the same anywhere a widget exposes named sub-parts — between asset rows on the picker, between details rows on the success screen, between method tiles on the picker. The Kit doesn't restrict what you mount alongside its parts; layout flows naturally because the parts use logical CSS.

Tip

When the interleaved JSX is a small UI atom — a button, a callout, an inline link, a tile — reach for @stridge/gateway-kit/ui rather than designing one from scratch. Button, Card, Alert, IconButton, SelectableTile, Text, Badge are the exact primitives the Kit's own widgets use, so your inserted element matches the surrounding flow's spacing, type scale, focus rings, and theming for free.

Default composition

Mount any widget bare — <AssetPicker /> — and you get the canonical sub-part composition. Equivalent to:

<AssetPicker>
  <AssetPicker.Header />
  <AssetPicker.Body>
    <AssetPicker.List />
  </AssetPicker.Body>
  <AssetPicker.Footer />
</AssetPicker>

When you want the default, the bare mount is enough — the widget renders the same tree internally.

When restyling is enough

If all you need is a different look, you don't need compound — reach for the skin tier instead and override the Kit's CSS variables. Drop into compound when restyling isn't enough and you need different structure — swap a screen, embed a step inline, drop a sub-part, interleave your own JSX.

Bootstrap with the shadcn registry

The fastest way to start a compound integration is to scaffold the canonical composition into your repo with the shadcn registry:

pnpm dlx shadcn@latest add @stridge/gateway-kit --registry https://registry.ui.stridge.com/r

This drops a components/stridge/ directory containing one file per FSM screen, each composed against the widgets on this page. You own the dropped files — restyle them, wrap them, delete the ones you don't need. The Kit still owns the wiring (FSM, gateway calls, settlement watcher); the dropped wrappers are just composition you'd otherwise hand-write.

See shadcn registry for the per-item file lists and how the dropped files map to the widgets below.

FSM routing — Deposit, Withdraw, and ActivityFlow

These ship only from /compound — they're the routing skeleton you mount inside a dialog (or inline) before any widget renders.

import { Deposit } from "@stridge/gateway-kit/deposit/compound"

<Deposit.Boundary>
  <Deposit.Guards>
    <Deposit.Steps>
      <Deposit.Step name="…">{/* widget or your component */}</Deposit.Step>
    </Deposit.Steps>
  </Deposit.Guards>
</Deposit.Boundary>
PartRole
Deposit.BoundaryError-boundary wrapper. onReset is auto-wired to useDeposit().close().
Deposit.GuardsGates the rest of the tree behind wallet-required / bootstrap-fatal / bootstrap-loading states. Render order: wallet → bootstrap-fatal → bootstrap-loading → children.
Deposit.StepsFSM router. Reads the state name returned by useDepositState() and renders only the matching child step.
Deposit.Step name="…"One leaf, keyed by FSM state name. Children render only when the step is active.

Step names: "deposit", "assetPicker", "amountEntry", "confirmDeposit", "transferCrypto", "onrampAmountEntry", "onrampCurrencyPicker", "onrampProviderPicker", "onrampConfirm", "onrampPaymentPending", "processing", "success", "error", "activityList", "activityDetail".

Note

The "deposit" step name is the method picker (wallet vs. transfer). Counterintuitive but stable — the FSM tag predates a clarifying rename.

The withdraw mirror — Withdraw.Boundary / .Guards / .Steps / .Step from @stridge/gateway-kit/withdraw/compound — works the same way. Step names: "form", "submitting", "inProgress", "success", "error", "activityList", "activityDetail". Standalone ActivityFlow routes "activityList" and "activityDetail" when its controller exists.

Deposit widget catalog

Every widget below ships from @stridge/gateway-kit/deposit/widgets. Each is zero-prop by default and accepts children to override the default sub-part composition.

Deposit

The method picker — wallet vs. transfer. Reads connected-wallet balance, brand name, and per-chain deposit addresses from the driver.

import { Deposit } from "@stridge/gateway-kit/deposit/widgets"

<Deposit>
  <Deposit.Header />
  <Deposit.Body>
    <Deposit.Methods />
  </Deposit.Body>
</Deposit>

Sub-parts: Header, Body, Methods, Method, Steps, Step, Dialog.

AssetPicker

The chain + token picker. Reads the source-asset list from the driver's balances entity.

import { AssetPicker } from "@stridge/gateway-kit/deposit/widgets"

<AssetPicker>
  <AssetPicker.Header />
  <AssetPicker.Body>
    <AssetPicker.List />
  </AssetPicker.Body>
  <AssetPicker.Footer />
</AssetPicker>

Sub-parts: Header, Body, List, Asset, Footer, Dialog.

AmountEntry

The numeric amount entry hero with optional percentage pills. Reads the live USD/token conversion off the driver's quote stream.

import { AmountEntry } from "@stridge/gateway-kit/deposit/widgets"

<AmountEntry>
  <AmountEntry.Header />
  <AmountEntry.Hero />
  <AmountEntry.Pills />
  <AmountEntry.Footer />
</AmountEntry>

ConfirmDeposit

The pre-broadcast confirmation step. Shows the source → destination route, the fee breakdown, and the confirm CTA.

import { ConfirmDeposit } from "@stridge/gateway-kit/deposit/widgets"

<ConfirmDeposit>
  <ConfirmDeposit.Header />
  <ConfirmDeposit.Body>
    <ConfirmDeposit.SourceAsset />
    <ConfirmDeposit.Arrow />
    <ConfirmDeposit.DestAsset />
    <ConfirmDeposit.Details />
  </ConfirmDeposit.Body>
</ConfirmDeposit>

TransferCrypto

The QR + address copy step rendered when the user picks the transfer method on the deposit-method picker.

ProcessingState

The watcher screen. Reads the live settlement status from the snapshot.

import { ProcessingState } from "@stridge/gateway-kit/deposit/widgets"

<ProcessingState>
  <ProcessingState.Header />
  <ProcessingState.StatusPill />
  <ProcessingState.Body>
    <ProcessingState.Details>
      <ProcessingState.Detail label="Tx hash" value="0x…" />
    </ProcessingState.Details>
  </ProcessingState.Body>
</ProcessingState>

SuccessState

The terminal success screen.

import { SuccessState } from "@stridge/gateway-kit/deposit/widgets"

<SuccessState>
  <SuccessState.Header />
  <SuccessState.Body>
    <SuccessState.Headline />
    <SuccessState.AssetValue />
    <SuccessState.Details>
      <SuccessState.Detail label="To" value="0x…ABCD" />
    </SuccessState.Details>
    <SuccessState.MoreDetails label="Receipt">
      {/* Collapsible block — explorer link, settlement id, … */}
    </SuccessState.MoreDetails>
  </SuccessState.Body>
  <SuccessState.Actions>
    {/* Action buttons — close, view receipt, copy hash */}
  </SuccessState.Actions>
</SuccessState>

ErrorState

The terminal error screen.

import { ErrorState } from "@stridge/gateway-kit/deposit/widgets"

<ErrorState>
  <ErrorState.Header />
  <ErrorState.Hero />
  <ErrorState.Body>
    <ErrorState.StatusValue />
    <ErrorState.Details />
    <ErrorState.HelpInfo />
  </ErrorState.Body>
  <ErrorState.Actions />
</ErrorState>

DepositStatusBanner

The inline status banner the deposit dialog renders alongside terminal states. Mount it anywhere inside <Deposit.Boundary> to surface in-progress settlements without taking over the screen.

DepositActivityList and DepositActivityDetail

The deposit-side activity surface. Mount inside the "activityList" / "activityDetail" FSM steps. The widgets read the rolling settlement history off the standalone activity driver — see Activity below.

DialogShell

Convenience export — pre-composed Dialog.Root + Dialog.Trigger + Dialog.Content with the kit's design tokens. Use it when you want to mount a single widget in its own dialog frame without wiring useDeposit() open-state manually.

Withdraw widget catalog

Every widget below ships from @stridge/gateway-kit/withdraw/widgets.

WithdrawForm

The form step — recipient, amount, receive-asset + receive-chain selectors, fee breakdown. The withdraw flow's centerpiece.

import { WithdrawForm } from "@stridge/gateway-kit/withdraw/widgets"

<WithdrawForm>
  <WithdrawForm.Header />
  <WithdrawForm.Body>
    <WithdrawForm.RecipientField />
    <WithdrawForm.AmountField />
    <WithdrawForm.ReceiveSelectors />
    <WithdrawForm.BreakdownCard />
  </WithdrawForm.Body>
  <WithdrawForm.Footer />
</WithdrawForm>

WithdrawInProgress

The processing screen. Mounts inside the "inProgress" FSM step.

WithdrawSuccess / WithdrawError

Terminal screens. Mount inside "success" and "error" FSM steps respectively.

WithdrawActivityList / WithdrawActivityDetail

The withdraw-side activity surface. See Activity.

Activity

The Kit's read-only Activity view covers settlements and Cash sessions. Deposit and Withdraw can enter it through their "activityList" / "activityDetail" steps. The built-in standalone <ActivityDialog />, ActivityFlow, and orchestrated Activity widgets use the Activity controller and driver derived from a configured flow with a resolved owner. The money-flow dialog need not be open. See Provider for owner resolution.

Widget imports:

// Deposit side
import {
  DepositActivityList,
  DepositActivityDetail,
} from "@stridge/gateway-kit/deposit/widgets"

// Withdraw side
import {
  WithdrawActivityList,
  WithdrawActivityDetail,
} from "@stridge/gateway-kit/withdraw/widgets"

import { ActivityList, ActivityDetail } from "@stridge/gateway-kit/activity/widgets"
import { ActivityFlow } from "@stridge/gateway-kit/activity/compound"

The orchestrated widgets read driver state and dispatch FSM actions internally. For host-owned history without a configured Deposit or Withdraw flow, import the bare presentation parts from /activity/compound:

import {
  Activity,
  ActivityDetail,
  ActivitySourceProvider,
  ActivityHeader,
  ActivityListHeader,
  ACTIVITY_SLOTS,
  ACTIVITY_ROW_SLOTS,
  type ActivitySourceValue,
  type ActivityPayload,
  type ActivityRowPayload,
} from "@stridge/gateway-kit/activity/compound"

Wrap bare <Activity /> or <ActivityDetail /> in <ActivitySourceProvider value={sourceValue}>. The host supplies an Activity entity, refresh, selectSettlement, and settlementContext; it owns history loading and navigation callbacks. This public presentation route needs neither money-flow controller. It also supplies no built-in Activity FSM, automatic polling, or activity.* emissions. Provide the Kit stylesheet and i18n context for the expected presentation. The in-flow and standalone widgets above wire the same bare parts to Kit-owned state when their controllers exist.

Putting it together — an embedded inline deposit

A trading-app sidebar that funds the margin balance without opening a modal. Widgets handle the per-step data; the FSM scaffolding from /compound routes between them.

TradeSidebar.tsx
import { useDeposit } from "@stridge/gateway-kit"
import { Deposit } from "@stridge/gateway-kit/deposit/compound"
import {
  AssetPicker,
  AmountEntry,
  ConfirmDeposit,
  ProcessingState,
  SuccessState,
  ErrorState,
} from "@stridge/gateway-kit/deposit/widgets"

export function TradeSidebar() {
  return (
    <aside>
      <h3>Fund margin</h3>
      <Deposit.Boundary>
        <Deposit.Guards>
          <Deposit.Steps>
            <Deposit.Step name="assetPicker"><AssetPicker /></Deposit.Step>
            <Deposit.Step name="amountEntry"><AmountEntry /></Deposit.Step>
            <Deposit.Step name="confirmDeposit"><ConfirmDeposit /></Deposit.Step>
            <Deposit.Step name="processing"><ProcessingState /></Deposit.Step>
            <Deposit.Step name="success"><SuccessState /></Deposit.Step>
            <Deposit.Step name="error"><ErrorState /></Deposit.Step>
          </Deposit.Steps>
        </Deposit.Guards>
      </Deposit.Boundary>
    </aside>
  )
}

Call useDeposit().open({ method: "wallet" }) from a "Fund" button and the sidebar renders inline — no modal, no portal, no extra wiring.

Going below widgets — bare parts

The same sub-part components ship from @stridge/gateway-kit/{flow}/compound without the data wiring. Reach for them when you're building a fully custom surface and want to drive the parts off your own snapshot reads — e.g. compose <AssetPicker.List /> against a balance list you transformed yourself, instead of the driver's balances entity.

import { AssetPicker } from "@stridge/gateway-kit/deposit/compound"
import { useDepositSnapshot } from "@stridge/gateway-kit"

function CustomAssetPicker() {
  const snapshot = useDepositSnapshot()
  const assets = transformToYourShape(snapshot.balances)
  return (
    <AssetPicker assets={assets} onAssetSelect={…}>
      <AssetPicker.Header />
      <AssetPicker.Body>
        <AssetPicker.List />
      </AssetPicker.Body>
    </AssetPicker>
  )
}

Most integrations never need this — the widgets cover the production cases. Use bare parts when you have a specific reason: custom balance source, A/B testing a different data shape, embedding outside the orchestrator entirely.

Data attributes on every sub-part

Every widget and every bare part is stamped with data-stridge-slot="<name>". Hosts target sub-parts from CSS without forking — see Data attributes & slots for the complete catalogue, with click-to-copy selectors.

Next

Was this page helpful?