Superbridge
Start typing to search...
You need a Rollie to use the Superbridge Widget.

Guides

State

Read and control the widget preferences and transfer state

Everything the user can change is readable and settable from your application, using the same controlled / uncontrolled pattern as a React input.

Two groups

PreferencesForm
WhatDurable settings — how the widget behavesWhat's being bridged right now
Keyslanguage, currency, slippage, testnetsfrom, to, amount, recipient
PersistedYes, by defaultNever
ChangesRarely, deliberatelyContinuously, as the user works
interface WidgetPreferences {
  language: string;
  currency: string;
  slippage: number;
  testnets: boolean;
}

interface WidgetForm {
  from: TokenRef | null;
  to: TokenRef | null;
  amount: string;
  recipient: string | null;
}

type TokenRef = ({ chainUid: string } | { chainId: string } | { chainKey: string })
              & { tokenAddress?: string };

Controlled and uncontrolled

Each group has three props, and every key supports all three.

preferences?: Partial<WidgetPreferences>;
defaultPreferences?: Partial<WidgetPreferences>;
onPreferencesChange?: (event: WidgetPreferencesChangeEvent) => void;

form?: Partial<WidgetForm>;
defaultForm?: Partial<WidgetForm>;
onFormChange?: (event: WidgetFormChangeEvent) => void;
Behaviour
Key present in preferences / formControlled. The widget renders your value and never changes it. Its UI still shows the control, but using it calls your handler. Feed the new value back or nothing happens.
Key omittedUncontrolled. The widget owns the value, seeded from default* and, for preferences, from storage. Your handler still fires, so you can observe without controlling.

This is decided per key, so you can pin one value and leave the rest to the user:

// `to` is fixed to your chain. `from`, `amount` and `recipient` stay the user's.
<Widget form={{ to: { chainUid: MY_CHAIN } }} />

Change handlers fire in both cases — see Reacting to changes.

null is a meaningful value. form={{ to: null }} means controlled, with nothing selected; omitting to entirely means the widget owns it.

Presetting

Presetting needs no handler and no feedback loop. This is a complete integration:

<Widget
  defaultForm={{
    from: { chainKey: "base", tokenAddress: USDC_BASE },
    to: { chainKey: "arbitrum", tokenAddress: USDC_ARB },
    amount: "100",
  }}
  defaultPreferences={{ currency: "GBP", slippage: 1 }}
/>

default* props are read once, when the widget initialises. Changing them later is ignored and will never overwrite what the user has since done.

A preset that references a chain by chainKey or chainId can't be resolved until the chain list has loaded. The widget applies it as soon as it becomes resolvable rather than dropping it, so presets behave the same on a cold load as a warm one.

Reacting to changes

Both handlers receive one object: the full next state, and a changes list describing exactly what moved.

/** One changed key paired with its correctly-typed value. */
type ChangeOf<T> = { [K in keyof T]-?: { key: K; value: T[K] } }[keyof T];

type WidgetPreferenceChange = ChangeOf<WidgetPreferences>;
//   | { key: "language"; value: string }
//   | { key: "currency"; value: string }
//   | { key: "slippage"; value: number }
//   | { key: "testnets"; value: boolean }

type WidgetFormChange = ChangeOf<WidgetForm>;

interface WidgetPreferencesChangeEvent {
  next: WidgetPreferences;
  changes: readonly WidgetPreferenceChange[];
}

interface WidgetFormChangeEvent {
  next: WidgetForm;
  changes: readonly WidgetFormChange[];
}

key is the discriminant, so checking it narrows value to the right type:

<Widget
  onPreferencesChange={({ changes }) => {
    for (const change of changes) {
      if (change.key === "slippage") setSlippage(change.value);   // number
      if (change.key === "testnets") setTestnets(change.value);   // boolean
    }
  }}
/>

Reading change.value without checking change.key first is a compile error, so you can't act on a value that didn't actually change.

If you just want the whole state, ignore changes entirely:

<Widget onFormChange={({ next }) => writeSelectionToUrl(next)} />

changes is a list because some actions move more than one key at once — swapping from and to, or switching to testnets clearing both tokens. You get one event per state transition rather than one per key, so you never see a half-applied state.

onFormChange is debounced for amount so it doesn't fire on every keystroke. Every other key reports immediately.

Observing without controlling

To mirror a value into your own app while leaving the widget in charge of it:

<Widget
  onPreferencesChange={({ changes }) => {
    for (const change of changes) {
      if (change.key === "currency") api.saveCurrency(change.value);
    }
  }}
/>

To keep your app and the widget in step — one locale across both — control it instead:

<Widget
  preferences={{ language: appLocale }}
  onPreferencesChange={({ next }) => setAppLocale(next.language)}
/>

The same shape mirrors the selected tokens into the URL:

<Widget
  form={selectionFromUrl}
  onFormChange={({ next }) => writeSelectionToUrl(next)}
/>

Persistence

Preferences the widget owns are remembered across reloads in localStorage. Form state never persists.

Preferences you control are not persisted by the widget. You're already the source of truth, so a stored copy would only be a second one that can disagree with yours. If you want a preference kept somewhere else — your user's account, a cookie, your own store — control it and persist it yourself:

function Bridge() {
  const [currency, setCurrency] = useState(user.currency);

  return (
    <Widget
      preferences={{ currency }}
      onPreferencesChange={({ changes }) => {
        for (const change of changes) {
          if (change.key !== "currency") continue;
          setCurrency(change.value);
          api.saveCurrency(change.value);
        }
      }}
    />
  );
}

That's the whole model: controlling a value is how you take over storing it.

State versus telemetry

Two kinds of callback, with no overlap:

  • onPreferencesChange and onFormChange are state. They are the callbacks that should drive what your application renders.
  • onEvent is telemetry. Past-tense analytics — a quote was selected, a transfer was attempted, a modal was opened. Fire-and-forget.

If you find yourself rendering from onEvent, the value you want is in one of the two state groups instead.

<Widget
  onEvent={(event) => {
    if (event.event === "transfer") analytics.track("bridge_transfer", event);
  }}
/>

The WidgetEvent union is exported from @superbridge/widget and discriminated on event, so narrowing gives you the right payload type.