Parent Controls
Gate buyer interaction with the embedded checkout from the parent page. Useful when your storefront has fields that must be filled (shipping address, terms acceptance, plan selector) before the buyer should be able to submit the embedded payment.
Isn't: a way to validate the buyer's data from the parent. Field-level validity is up to your form. The parent signals "ready" or "not ready" — Throttle just enforces the gate.
The submitDisabled prop
Both <PaymentEmbed /> and <CheckoutEmbed /> in @usethrottle/checkout-react accept a controlled submitDisabled?: boolean prop. When it flips to true, the SDK posts a parent → iframe command and the iframe renders a full-area overlay. When it flips back to false, the overlay disappears.
'use client';
import { useState } from 'react';
import { PaymentEmbed } from '@usethrottle/checkout-react';
import { useForm } from 'react-hook-form';
export function CheckoutPage({ sessionId }: { sessionId: string }) {
const { register, watch } = useForm({ mode: 'onChange' });
const email = watch('email');
const name = watch('name');
const formValid = !!email && !!name;
return (
<>
<form>
<input {...register('name', { required: true })} placeholder="Full name" />
<input {...register('email', { required: true })} placeholder="Email" type="email" />
</form>
<PaymentEmbed
sessionId={sessionId}
parentOrigin="https://shop.example.com"
submitDisabled={!formValid}
onSucceeded={({ orderId }) => router.push(`/orders/${orderId}`)}
/>
</>
);
}Defaults
- Omitting
submitDisabledentirely keeps the embed enabled (backward compatible). The SDK never posts a parent command in this case. submitDisabled={false}and omitting the prop are equivalent at runtime, but passingfalseexplicitly opts your embed into the parent-control protocol — useful when your form starts valid (e.g., everything pre-filled).- The first parent command is sent after the iframe posts
throttle.ready. You don't need to wait for this in your component code — the SDK buffers and re-sends.
Composes with collect-flag auto-disable
The iframe also auto-disables the Pay button until every field required by the session's collect flags (shipping address, billing address) is complete. Parent-set submitDisabled composes additively with that validation gate: the button stays disabled while either the parent says "not ready" or the iframe's own validation hasn't passed.
Practically: you don't need to mirror the iframe's address validation in your parent form. Only gate on parent-controlled state (terms checkbox, plan selection, out-of-band eligibility checks). Throttle handles the rest.
Without React
The SDK is a thin wrapper over postMessage. Vanilla JS, Vue, Svelte, Angular — anything that mounts an iframe can post the same shape.
// Direct postMessage when not using @usethrottle/checkout-react.
// Useful for vanilla JS storefronts or non-React frameworks.
const iframe = document.querySelector('#throttle-checkout');
const throttleOrigin = 'https://checkout.usethrottle.dev';
function setSubmitDisabled(disabled) {
iframe.contentWindow.postMessage(
{
source: 'throttle-parent',
version: 1,
type: 'set-submit-disabled',
disabled,
},
throttleOrigin,
);
}
// Wire to your form's validity state:
form.addEventListener('input', () => {
setSubmitDisabled(!form.checkValidity());
});Message contract
The parent → iframe channel uses source: 'throttle-parent' to disambiguate from the iframe → parent events (which use source: 'throttle'). The iframe ignores anything that doesn't match.
{
"source": "throttle-parent", // distinguishes parent commands from
// throttle's own iframe → parent events
"version": 1, // protocol version; bumps are additive
"type": "set-submit-disabled",
"disabled": true | false
}Future commands will extend the same envelope; the protocol is additive.
Security
- The iframe verifies
event.originmatches the merchant-allowed parent origin before reading the message. A different page can't steal control. - Setting
disabled: falsefrom an attacker doesn't bypass anything — it simply removes the overlay. The buyer still has to actually submit the form, and Throttle still has its own server-side validation on the/completeroute. - The overlay is cosmetic, not a security boundary. It blocks the buyer's clicks but doesn't prevent scripts. Treat
submitDisabledas a UX gate, not a server-side enforcement primitive.
What does it actually do inside the iframe?
The iframe's EmbedShell listens for the parent command and renders a positioned overlay (semi-transparent white + cursor not-allowed) covering the embed content when disabled. The overlay sits at z-index 50 so it covers both Throttle's native UI and any nested provider iframe (the card form). The buyer sees what they've typed but can't click anything.
We chose "overlay over the iframe" instead of "disable the submit button only" because the provider embed renders its own submit button inside a nested iframe we don't control. Greying the entire interaction surface gives the same UX outcome with stable semantics.
Common uses
- Address before pay. Your parent page renders a shipping address form. Pass
submitDisabled={!addressValid}. - Terms acceptance. A "I agree to terms" checkbox above the embed. Pass
submitDisabled={!checked}. - Plan selection. Buyer must pick a plan before paying. Pass
submitDisabled={!selectedPlan}. - Async eligibility. You're waiting on a backend check (e.g., calling
subscriptions/eligibility-check). Disable until the result returns.
Next
- Embedded Checkout — full embed lifecycle and event reference.