New:Introducing Socket Scanning for VS Code Marketplace Extensions.Learn more →
Get Started

@layerhop/widget

Package Overview
Dependencies
Maintainers
1
Versions
6
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@layerhop/widget

Embed the LayerHop cross-chain swap widget and checkout in any site: React components, framework-agnostic mount()/openCheckout() and drop-in <script> tags

latest
Source
npmnpm
Version
0.7.0
Version published
Weekly downloads
630
Maintainers
1
Weekly downloads
 
Created
Source

@layerhop/widget

npm license docs

Embed the LayerHop cross-chain swap card in any site. The widget is an <iframe> of https://layerhop.com/embed, configured through URL params. It reports back to your page through postMessage events. Both the params and the events follow the widget reference.

Design it visually first at layerhop.com/developers/widget: the builder previews the real widget and generates the code for each option below.

There are three ways to use it:

  • the framework-agnostic mount()
  • the React component <LayerHopWidget />
  • a drop-in <script> tag

Install

npm install @layerhop/widget
# React is an optional peer dependency (>=18), needed only for @layerhop/widget/react

React

import { LayerHopWidget } from "@layerhop/widget/react";

export function Deposit() {
  return (
    <LayerHopWidget
      srcChain="arbitrum"
      srcToken="USDC"
      dstChain="base"
      dstToken="USDC"
      lockDst
      theme="dark"
      accent="#22d3ee"
      integrator="myapp"
      onQuote={(q) => console.log("quote", q.dstAmount)}
      onStatus={(s) => s.state === "filled" && console.log("done", s.srcTxHash)}
      className="my-widget"
    />
  );
}
  • When a URL prop changes, the iframe reloads, but only if the resulting URL actually changes.
  • Callback props can change freely without reloading the iframe.
  • Changing baseUrl, autoResize, height, maxWidth or title remounts the iframe.

Vanilla JS / any framework

import { mount } from "@layerhop/widget";

const widget = mount("#swap", {
  srcChain: "base",
  dstChain: "solana",
  dstToken: "SOL",
  amount: "100",
  onReady: ({ version }) => console.log("embed", version),
  onSubmitted: ({ srcTxHash }) => console.log("tx", srcTxHash),
  onError: ({ code, message }) => console.warn(code, message),
});

widget.update({ amount: "250", theme: "light" }); // merges options; reloads only if the URL changes
// With mount(..., { liveUpdates: true }) amount, token, theme and color changes apply in place.
widget.destroy(); // removes the iframe and the message listener

You can also build an embed URL yourself, for example to use in your own <iframe>:

import { buildEmbedUrl } from "@layerhop/widget";
buildEmbedUrl({ srcChain: "base", lockSrc: true }); // https://layerhop.com/embed?srcChain=base&lockSrc=1

Script tag (no build step)

<div data-layerhop-widget data-src-chain="base" data-dst-chain="solana" data-theme="dark"></div>
<!-- once @layerhop/widget is published to npm -->
<script src="https://unpkg.com/@layerhop/widget@0.7.0/dist/embed.js"></script>
<script>
  document.querySelector("[data-layerhop-widget]")
    .addEventListener("layerhop:quote", (e) => console.log(e.detail));

  // or programmatically:
  const w = window.LayerHop.mount("#other", { srcChain: "base", onQuote: console.log });
</script>

How auto-init works

  • embed.js mounts every [data-layerhop-widget] element once the DOM is ready.
  • Each option is a kebab-case data attribute: data-src-chain, data-src-token, data-dst-chain, data-dst-token, data-amount, data-recipient, data-slippage-bps, data-theme, data-preset, data-accent, data-accent-text, data-bg, data-surface, data-surface-alt, data-text, data-text-muted, data-border-color, data-success, data-warning, data-danger, data-radius, data-radius-inner, data-font, data-font-scale, data-density, data-width, data-shadow, data-border, data-header-title, data-hide-header, data-logo, data-button-label, data-show-usd, data-from-chains, data-to-chains, data-exclude-chains, data-from-tokens, data-to-tokens, data-lock-src, data-lock-dst, data-lock-amount, data-lock-recipient, data-hide-routes, data-hide-settings, data-hide-history, data-hide-powered-by, data-locale, data-allow-parent-origin, data-integrator, data-base-url, data-height, data-max-width, data-auto-resize and data-live-updates. Lists are comma-separated.
  • Flags are on when the attribute is empty, 1, true or yes, and off for 0.
  • Events are re-dispatched on the element as bubbling layerhop:<type> CustomEvents, with the payload in event.detail.
  • For elements you add later, call window.LayerHop.autoInit(). It skips elements that are already mounted.
  • window.LayerHop exposes mount, buildEmbedUrl, autoInit, getWidget, LayerHopWidgetError, DEFAULT_BASE_URL, PRESETS and PRESET_IDS.

Options

Every option maps to one embed URL param of the same name, except borderColor (the border param with a hex value) and headerTitle (the title param). See the widget reference.

Swap

OptionTypeNotes
srcChain / dstChainstring | numberchain slug (base) or id
srcToken / dstTokenstringsymbol, address or mint
amountstringhuman units, e.g. "100" or "0.5"
recipientstringprefilled, but editable unless lockRecipient
slippageBpsnumberinteger from 0 to 10000
integratorstringmust match [a-z0-9-]{1,32}. Your partner id: registered partners earn their partner fee on every swap (guide)

Theme

OptionTypeNotes
theme"dark" | "light" | "auto"default dark (or the preset's own default); auto follows the visitor's setting
preset"midnight" | "paper" | "lime" | "ocean" | "mono"a full dark + light palette; explicit colors override it
accentstringhex; 3, 4, 6 or 8 digits accepted (the embed uses 3 or 6)
accentText, bg, surface, surfaceAlt, text, textMuted, borderColor, success, warning, dangerstringhex, 3 or 6 digits, # optional. Unreadable accentText is replaced automatically
radiusnumbercard corner radius in px (the embed caps it at 40)
radiusInnernumberfields and buttons, 0–32 px
font"inter" | "system" | "space-grotesk" | "ibm-plex-sans" | "dm-sans" | "manrope"the last three load from Google Fonts only when chosen
fontScalenumber0.875–1.125
density"compact" | "comfortable"
widthnumbermax card width, 320–720 px; also widens the iframe (width + 16) unless maxWidth is set
shadow"none" | "soft" | "strong"
borderbooleanfalse removes the card outline, true strengthens it. Can't be combined with borderColor

Content and behaviour

OptionTypeNotes
headerTitlestringheader text, up to 32 characters (URL param title; title stays the iframe title)
logostringhttps image URL shown next to the title
buttonLabelstringmain button label once ready, up to 24 characters
hideHeaderbooleanhides title, wallet button, history and settings
showUsdbooleandefault true
fromChains / toChains / excludeChains(string | number)[]chain allow-lists / deny-list
fromTokens / toTokensstring[]"chain:addressOrSymbol" entries, or bare symbols
lockSrc / lockDstbooleanlocks that side's chain and token
lockAmountboolean
lockRecipientbooleanrequires recipient
hideRoutes / hideSettings / hideHistoryboolean
hidePoweredBybooleanrequires integrator; the embed honours it once your id is confirmed as a registered partner
locale"en"
allowParentOriginstringorigin allowed to send setParams; liveUpdates sets it for you

Mount only

OptionTypeNotes
baseUrlstringdefault https://layerhop.com; /embed is appended. For local dev, use http://localhost:3000
autoResizebooleandefault true; follows resize events
heightnumberinitial height, default 640 px
maxWidthnumber | stringdefault 480 px (or width + 16); the width is always 100%
titlestringthe iframe title, default "LayerHop swap"
liveUpdatesbooleanupdate() sends amount, token, theme and color changes to the running widget instead of reloading it

Invalid values throw a LayerHopWidgetError when you call buildEmbedUrl, mount or update. This makes integration bugs show up early. The embed page itself silently ignores bad params.

The iframe always gets color-scheme: normal, which matches the embed page. A different value (for example inherited from a page with color-scheme: dark) makes browsers paint an opaque box behind the transparent widget.

Presets

import { PRESETS, PRESET_IDS } from "@layerhop/widget"; // also exported as WIDGET_PRESETS
PRESETS.ocean.dark.accent; // "38bdf8"

Each preset has label, description, defaultTheme and full dark / light palettes (hex without #), handy for matching the rest of your page to the widget.

Recipes

Deposit into your dApp. Any token on any chain in, USDC on Base out, always to your contract:

<LayerHopWidget
  dstChain="base"
  dstToken="USDC"
  lockDst
  recipient="0xYourDepositContract"
  lockRecipient
  headerTitle="Deposit to Acme"
  buttonLabel="Deposit"
  integrator="acme"
  onStatus={(s) => s.state === "filled" && refreshBalance()}
/>

Single-chain swap. Solana only, best route only:

mount("#swap", {
  fromChains: ["solana"],
  toChains: ["solana"],
  srcChain: "solana",
  srcToken: "USDC",
  dstChain: "solana",
  dstToken: "SOL",
  hideRoutes: true,
});

Branded theme. A preset as the base, your accent, font and logo, following light/dark:

<div
  data-layerhop-widget
  data-preset="midnight"
  data-theme="auto"
  data-accent="ff5c8a"
  data-accent-text="ffffff"
  data-font="manrope"
  data-radius="20"
  data-radius-inner="12"
  data-logo="https://acme.xyz/logo.svg"
  data-header-title="Acme Swap"
></div>

Drive it from your page. With liveUpdates, update() changes the running widget in place:

const w = mount("#swap", { liveUpdates: true, dstChain: "base", dstToken: "USDC" });
priceInput.addEventListener("input", () => w.update({ amount: priceInput.value }));

Events

The embed posts { source: "layerhop", type, payload } to the parent window.

The widget accepts a message only if all of these are true:

  • it comes from its own iframe's window
  • its origin is the baseUrl origin
  • it has source === "layerhop"
CallbacktypePayload
onReadyready{ version }
onQuotequote{ provider, srcChain, dstChain, amount, dstAmount }
onSubmittedsubmitted{ provider, srcChainId, srcTxHash }
onStatusstatus{ provider, srcTxHash, state } where state is pending, filled, refunded or failed
onErrorerror{ code, message }
onResizeresize{ height } in px; the iframe is resized automatically unless autoResize: false
onRoutesroutes{ count, provider }: routes that answered and the best one's provider
onWalletConnectedwalletConnected{ vm, address } with vm "evm" or "svm"
onWalletDisconnectedwalletDisconnected{ vm, address }
onAmountChangedamountChanged{ amount, srcChain, srcToken }, debounced, user edits only
onEventanythe full { source, type, payload } message

All payload types are exported: ReadyEvent, QuoteEvent, SubmittedEvent, StatusEvent, ErrorEvent, ResizeEvent, RoutesEvent, WalletEvent, AmountChangedEvent, WidgetEventMap and WidgetMessage. Payloads never contain secrets. The new events need embed protocol 1.1 (ready.version ≥ 1.1.0).

Checkout: accept payments on your site

The same package embeds LayerHop Checkout: the payer picks any token on any chain, you're paid in USDC or USDT, and they never leave your page. The checkout is an iframe of https://layerhop.com/pay/{sessionId}?embed=1|modal.

Payment button, no code (guide):

<script src="https://layerhop.com/checkout.js" async></script>

<button data-layerhop-product="prod_…">Pay with crypto</button>
  • data-layerhop-product="prod_…" starts a new session for the product on click; data-layerhop-session="cs_…" opens an existing one.
  • data-layerhop-reference="order_1042", data-layerhop-metadata='{"plan":"pro"}' or data-layerhop-metadata-plan="pro" (products only).
  • data-layerhop-mode="inline" renders the checkout inside the element instead of opening a modal.
  • An empty <button> gets the label "Pay with crypto". On a link (<a href="https://layerhop.com/buy/prod_…">), the href is the no-JS fallback.
  • Events bubble from the element: layerhop:ready, layerhop:status, layerhop:paid, layerhop:cancel, layerhop:close, layerhop:error (event.detail has sessionId, plus status, session or error).
  • data-layerhop-base-url / data-layerhop-api-url on the <script> tag (or window.LayerHopCheckoutConfig = { baseUrl, apiUrl }) point it elsewhere, e.g. a local LayerHop.

JavaScript (window.LayerHopCheckout from checkout.js, or import { openCheckout } from "@layerhop/widget"):

const checkout = LayerHopCheckout.open({
  sessionId: "cs_…",                 // created on your server; or productId: "prod_…"
  onPaid: (session) => showThanks(session.id),
  onClose: () => console.log("closed"),
});
// checkout.close(); await checkout.sessionId;

LayerHopCheckout.mount("#pay", { productId: "prod_…", reference: "order_1042" }); // inline, auto-resizing
const session = await LayerHopCheckout.createProductSession("prod_…", { reference: "order_1042" });

React

import { LayerHopCheckout, useLayerHopCheckout } from "@layerhop/widget/react";

<LayerHopCheckout productId="prod_…" reference="order_1042" onPaid={(s) => router.push(`/thanks?session=${s.id}`)} />;

const checkout = useLayerHopCheckout({ onPaid });
<button onClick={() => checkout.open({ sessionId })}>Pay with crypto</button>;

Options: sessionId or productId, reference, metadata, baseUrl (default https://layerhop.com), apiUrl (default https://api.layerhop.com), title, and callbacks onReady, onPaid(session), onCancel, onClose (the modal closed, for any reason), onStatus(status), onError(error). mountCheckout also takes autoResize, height and maxWidth. Both return { sessionId: Promise<string>, iframe, close() }.

The modal is an aria-modal dialog: focus stays inside, Escape and the backdrop close it, the page doesn't scroll behind it, and it's a full-height sheet on phones. After paid it stays open on the receipt; close it from onPaid if you prefer. Fulfil orders on the checkout.session.completed webhook, not only on onPaid.

Messages (for a hand-rolled iframe): the frame posts { source: "layerhop-checkout", version: 1, type, sessionId, ... } to its parent with types ready, resize (height), status (status), paid (session), cancel and close (modal only). Pass origin=<your origin> in the iframe URL so messages are sent to your origin only, and check event.origin === "https://layerhop.com" and event.source === iframe.contentWindow. The embedded checkout never redirects to your successUrl; your page decides what happens next.

Development

pnpm --filter @layerhop/widget typecheck
pnpm --filter @layerhop/widget test
pnpm --filter @layerhop/widget build    # dist/: ESM + CJS + .d.ts, dist/embed.js and dist/checkout.js (IIFE)

To run the demo, start apps/web on :3000 and build this package. Then serve packages/widget (for example with npx serve packages/widget) and open /example/.

License: UNLICENSED until DECISIONS D-007 is resolved.

Keywords

layerhop

FAQs

Package last updated on 19 Oct 2026

Related posts