Migrate Select from Polaris React
Replace Polaris React Select with s-select and explicit s-option children. Keep the selected value in form or app state, and preserve the stable values that backend operations expect.
If the app renders this controlled field through React, upgrade to React 19 first. React 18 doesn't provide the custom-element property and event behavior this example relies on. If you can't upgrade yet, leave the controlled Polaris React field in place during this migration slice.
Anchor to Migrate a select fieldMigrate a select field
The destination moves the options descriptor array into child elements and keeps validation on the field.
Migrating a required shipping-origin field
Polaris web components
<s-select
label="Shipping origin"
error="Select your primary shipping location to calculate accurate rates for customers"
required
value=""
>
<s-option value="">Select a country</s-option>
<s-option value="ca">Canada</s-option>
<s-option value="us">United States</s-option>
<s-option value="mx">Mexico</s-option>
<s-option value="uk">United Kingdom</s-option>
</s-select>Polaris React
import {Select} from '@shopify/polaris';
export function ShippingOrigin({
selectedOrigin = '',
validationError = 'Select your primary shipping location to calculate accurate rates for customers',
onChange,
}) {
return (
<Select
label="Shipping origin"
requiredIndicator
value={selectedOrigin}
error={validationError}
onChange={onChange}
options={[
{label: 'Select a country', value: ''},
{label: 'Canada', value: 'ca'},
{label: 'United States', value: 'us'},
{label: 'Mexico', value: 'mx'},
{label: 'United Kingdom', value: 'uk'},
]}
/>
);
}Preview
Anchor to Replace option descriptorsReplace option descriptors
| Polaris React | Polaris web components | Migration notes |
|---|---|---|
options | Explicit s-option children | Move label to child text and keep value. |
| Option group | s-option-group with s-option children | Keep meaningful group labels rather than flattening a long list. |
Option disabled | disabled on s-option | Preserve unavailable choices without changing their values. |
label | label | Keep a visible field label unless nearby context makes it redundant. |
labelHidden | labelAccessibilityVisibility="exclusive" | Keep an accessible label. |
helpText | details | Keep supporting instructions with the field. |
placeholder | placeholder | Use it as a prompt, not as a substitute for the label. |
requiredIndicator | required | The property adds semantics and an indicator. Set error when validation fails. |
value | value | Pass the controlled selected value. |
onChange(selected, id) | onChange(event) | Read event.currentTarget.value. |
error | error | Pass a specific recovery message as a string. |
disabled | disabled | Preserve the condition that controls availability. |
Use the same option values before and after migration unless the backend contract is changing deliberately. Changing a display label shouldn't change the serialized value.
Anchor to Choose the right choice controlChoose the right choice control
Use s-select for one compact choice from a list. Use s-choice-list when merchants benefit from seeing every option or when choices need individual descriptions. Don't use a select for actions such as Create, Delete, or Export.
For a controlled select, update app state from the change event and pass that value back to the component. For a native form submission, keep a unique name and verify that the submitted value matches the backend input.
Anchor to Test the migrationTest the migration
- Choose every enabled option and verify its stable value reaches app state and form submission.
- Test placeholders, required validation, and field-level errors.
- Verify disabled fields and disabled options can't be selected.
- Test long and localized labels at narrow widths.
- Reload editing forms and confirm the current value restores correctly.
Anchor to Remove Polaris ReactRemove Polaris React
After every call site is migrated, remove Select, descriptor-only option builders, and callback adapters. Remove @shopify/polaris only after no other route in scope imports it.