Skip to main content
Migrate to Polaris

Version 2025-07 is the last API version to support React-based UI components. Later versions use web components, native UI elements with built-in accessibility, better performance, and consistent styling with Shopify's design system. Check out the migration guide to upgrade your extension.

Form

The Form component wraps form controls and manages submission and reset behavior through the Shopify admin's save bar.

Unlike HTML forms, Form doesn't automatically submit data using HTTP—you must handle form data programmatically in your onSubmit callback. For Shopify Functions configuration forms, use FunctionSettings.

Support
Targets (46)

Supported targets


Props for the Form component, a container that groups related input fields and manages submission and reset behavior through the save bar.

Anchor to onReset
onReset
() => void | Promise<void>
required

A callback that fires when the form is reset. This is triggered by the save bar's Discard action. Use it to revert your extension's state back to its last saved values. Return a Promise if you need to perform async cleanup.

Anchor to onSubmit
onSubmit
() => void | Promise<void>
required

A callback that fires when the form is submitted. This is triggered by the save bar's Save action or by a submit button inside the form. Return a Promise if you need to perform async work (like a network request), and the Shopify admin will wait for it to resolve before treating the submission as complete.

string

A unique identifier for the form. Use this when you need to reference the form from outside its tree, for example from a submit button that isn't a child of the form.


Anchor to Submit product metadata formSubmit product metadata form

Submit a warehouse SKU and storage location, or cancel to close the modal. This example uses Form with onSubmit and onReset callbacks around two TextField inputs, where onReset calls close() to dismiss the modal.

Submit product metadata form

Submit a warehouse SKU and storage location, or cancel to close the modal. This example uses `Form` with `onSubmit` and `onReset` callbacks around two [TextField](/docs/api/admin-extensions/2025-07/ui-components/forms/textfield) inputs, where `onReset` calls `close()` to dismiss the modal.

Submit product metadata form

import {reactExtension, useApi, Form, TextField, Button, BlockStack} from '@shopify/ui-extensions-react/admin';

function App() {
const {data, close} = useApi('admin.product-details.action.render');
const productId = data.selected[0]?.id;

return (
<Form
onSubmit={async () => {
await fetch('/api/products/metadata', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({productId}),
});
close();
}}
onReset={() => close()}
>
<BlockStack gap>
<TextField label="Warehouse SKU" name="warehouseSku" required />
<TextField label="Storage location" name="location" />
<Button variant="primary">Save product metadata</Button>
</BlockStack>
</Form>
);
}

export default reactExtension(
'admin.product-details.action.render',
() => <App />,
);
import {extension, Form, TextField, Button, BlockStack} from '@shopify/ui-extensions/admin';

export default extension(
'admin.product-details.action.render',
(root, api) => {
const {data, close} = api;
const productId = data.selected[0]?.id;

const form = root.createComponent(Form, {
onSubmit: async () => {
await fetch('/api/products/metadata', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({productId}),
});
close();
},
onReset: () => {
close();
},
});

const stack = root.createComponent(BlockStack, {gap: true});

const skuField = root.createComponent(TextField, {
label: 'Warehouse SKU',
name: 'warehouseSku',
required: true,
});

const locationField = root.createComponent(TextField, {
label: 'Storage location',
name: 'location',
});

const submitButton = root.createComponent(
Button,
{variant: 'primary'},
'Save product metadata',
);

stack.appendChild(skuField);
stack.appendChild(locationField);
stack.appendChild(submitButton);
form.appendChild(stack);
root.appendChild(form);
},
);

Anchor to Handle form submit and cancelHandle form submit and cancel

Handle cancellation with the onReset callback to close the modal without saving. This example pairs submit and reset actions using InlineStack to right-align the cancel and save buttons, following standard dialog patterns.

Handle form submit and cancel

import {reactExtension, useApi, Form, TextField, Button, InlineStack, BlockStack} from '@shopify/ui-extensions-react/admin';

function App() {
const {data, close} = useApi('admin.product-details.action.render');
const productId = data.selected[0]?.id;

return (
<Form
onSubmit={async () => {
await fetch('/api/products/shipping', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({productId}),
});
close();
}}
onReset={() => close()}
>
<BlockStack gap>
<TextField label="Package weight (kg)" name="weight" />
<TextField label="Dimensions (L×W×H cm)" name="dimensions" />
<InlineStack gap inlineAlignment="end">
<Button variant="tertiary" accessibilityRole="reset">Cancel</Button>
<Button variant="primary" accessibilityRole="submit">Save shipping info</Button>
</InlineStack>
</BlockStack>
</Form>
);
}

export default reactExtension(
'admin.product-details.action.render',
() => <App />,
);
import {extension, Form, TextField, Button, InlineStack, BlockStack} from '@shopify/ui-extensions/admin';

export default extension(
'admin.product-details.action.render',
(root, api) => {
const {data, close} = api;
const productId = data.selected[0]?.id;

const form = root.createComponent(Form, {
onSubmit: async () => {
await fetch('/api/products/shipping', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({productId}),
});
close();
},
onReset: () => {
close();
},
});

const stack = root.createComponent(BlockStack, {gap: true});

const weightField = root.createComponent(TextField, {
label: 'Package weight (kg)',
name: 'weight',
});

const dimensionsField = root.createComponent(TextField, {
label: 'Dimensions (L×W×H cm)',
name: 'dimensions',
});

const actions = root.createComponent(InlineStack, {gap: true, inlineAlignment: 'end'});
const resetButton = root.createComponent(
Button,
{variant: 'tertiary', accessibilityRole: 'reset'},
'Cancel',
);
const submitButton = root.createComponent(
Button,
{variant: 'primary', accessibilityRole: 'submit'},
'Save shipping info',
);
actions.appendChild(resetButton);
actions.appendChild(submitButton);

stack.appendChild(weightField);
stack.appendChild(dimensionsField);
stack.appendChild(actions);
form.appendChild(stack);
root.appendChild(form);
},
);

Anchor to Organize form with sectionsOrganize form with sections

Organize complex forms with Section components to group related fields together. This example splits a fulfillment provider setup into "Provider details" and "Contact information" sections, making long forms easier to scan.

Organize form with sections

import {reactExtension, useApi, Form, TextField, EmailField, Section, Button, BlockStack} from '@shopify/ui-extensions-react/admin';

function App() {
const {data, close} = useApi('admin.product-details.action.render');
const productId = data.selected[0]?.id;

return (
<Form
onSubmit={async () => {
await fetch('/api/fulfillment/setup', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({productId}),
});
close();
}}
onReset={() => close()}
>
<BlockStack gap>
<Section heading="Provider details">
<TextField label="Provider name" name="providerName" required />
<TextField label="API endpoint" name="endpoint" />
</Section>
<Section heading="Contact information">
<TextField label="Contact name" name="contactName" />
<EmailField label="Contact email" name="contactEmail" />
</Section>
<Button variant="primary">Set up provider</Button>
</BlockStack>
</Form>
);
}

export default reactExtension(
'admin.product-details.action.render',
() => <App />,
);
import {extension, Form, TextField, EmailField, Section, Button, BlockStack} from '@shopify/ui-extensions/admin';

export default extension(
'admin.product-details.action.render',
(root, api) => {
const {data, close} = api;
const productId = data.selected[0]?.id;

const form = root.createComponent(Form, {
onSubmit: async () => {
await fetch('/api/fulfillment/setup', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({productId}),
});
close();
},
onReset: () => {
close();
},
});

const stack = root.createComponent(BlockStack, {gap: true});

const providerSection = root.createComponent(Section, {heading: 'Provider details'});
const nameField = root.createComponent(TextField, {label: 'Provider name', name: 'providerName', required: true});
const endpointField = root.createComponent(TextField, {label: 'API endpoint', name: 'endpoint'});
providerSection.appendChild(nameField);
providerSection.appendChild(endpointField);

const contactSection = root.createComponent(Section, {heading: 'Contact information'});
const contactName = root.createComponent(TextField, {label: 'Contact name', name: 'contactName'});
const contactEmail = root.createComponent(EmailField, {label: 'Contact email', name: 'contactEmail'});
contactSection.appendChild(contactName);
contactSection.appendChild(contactEmail);

const submitButton = root.createComponent(Button, {variant: 'primary'}, 'Set up provider');

stack.appendChild(providerSection);
stack.appendChild(contactSection);
stack.appendChild(submitButton);
form.appendChild(stack);
root.appendChild(form);
},
);

  • Form doesn't provide built-in form state management or validation. You must manage field values, errors, and dirty state yourself.
  • The save bar appearance and behavior is controlled by the Shopify admin. You can't customize its position, text, or button labels.
  • Form doesn't support nested forms. Only one Form component should be used per extension view.

Was this page helpful?