{"id":17076,"plugin_id":"plugins_6a701c7b1f9481919cf7c7448ddc1bd4","kind":"skill","collection_source":null,"comparison_source":null,"observed_at":"2026-09-30T23:13:56.721Z","digest":"719d8d1aef268bc39f2491e04b86e35b01b140366c04574be87bcb430deaa977","against":null,"payload":{"description":"Use when reviewing or writing Polaris UI for common design mistakes — using deprecated Stack instead of BlockStack/InlineStack, modal overuse, blocking validation, wrong tone (success/critical/warning/info), custom CSS overrides instead of tokens, off-brand colors, mis-sized cards, missing helpText, no FormLayout, mobile responsive failures, accessibility failures inside Polaris components, drifting between Polaris versions. Triggers: 'polaris mistake', 'polaris anti-pattern', 'polaris stack deprecated', 'polaris design review', 'shopify ui code review', 'polaris best practice', 'polaris vs custom', 'polaris tokens'.","included_files":[],"name":"ux-polaris-antipatterns","skill_md_contents":"---\nname: ux-polaris-antipatterns\ndescription: \"Use when reviewing or writing Polaris UI for common design mistakes — using deprecated Stack instead of BlockStack/InlineStack, modal overuse, blocking validation, wrong tone (success/critical/warning/info), custom CSS overrides instead of tokens, off-brand colors, mis-sized cards, missing helpText, no FormLayout, mobile responsive failures, accessibility failures inside Polaris components, drifting between Polaris versions. Triggers: 'polaris mistake', 'polaris anti-pattern', 'polaris stack deprecated', 'polaris design review', 'shopify ui code review', 'polaris best practice', 'polaris vs custom', 'polaris tokens'.\"\n---\n\n# Polaris UI Anti-Patterns (v12+)\n\nA field guide to the 20 most common ways Shopify app teams break the Polaris contract — and the exact fix for each. Use this when reviewing a PR, writing a new screen, or auditing an app pre-submission for Built for Shopify.\n\nThe principle behind every rule: **Shopify admin is a shared design surface.** Merchants have already learned its patterns. The closer your app feels to the rest of admin, the higher your install-to-activation rate. The further you drift, the more your app feels like a third-party graft — and the more support tickets you get from confused merchants.\n\n---\n\n## 1. When to use\n\nPull this skill in when you are:\n\n- Reviewing a pull request that touches `@shopify/polaris` components\n- Writing a new screen and unsure whether a custom component is warranted\n- Migrating from Polaris 10 or 11 to 12+\n- Preparing an app for App Store submission or Built for Shopify certification\n- Debugging \"why does our app look off-brand?\" complaints\n- Diagnosing accessibility audit failures\n- Deciding between `<Modal>`, `<Banner>`, `<Toast>`, or inline error\n- Choosing between custom CSS and design tokens\n\nIf the user mentions Polaris, Stack, BlockStack, design tokens, `--p-color-*`, `className` on Polaris components, Modal overuse, or \"this feels off-brand\" — trigger this skill.\n\n---\n\n## 2. The 20 anti-patterns, ranked by severity\n\nRanked by how badly each one degrades the merchant experience (1 = catastrophic, 20 = nit). Fixes are in the \"do this instead\" column.\n\n| # | Anti-pattern | Severity | Do this instead |\n|---|---|---|---|\n| 1 | Using `<Stack>` / `<LegacyStack>` / `<VerticalStack>` / `<HorizontalStack>` | Build-breaking on v12+ | `<BlockStack>` (vertical) or `<InlineStack>` (horizontal). Run `npx @shopify/polaris-migrator` |\n| 2 | Wrapping Polaris components in custom `className` for styling | High — most Polaris components do not accept `className` | Use the component's built-in props (`tone`, `variant`, `padding`, `gap`). For exceptions, use `<Box>` with token props |\n| 3 | Hex codes (`color: #008060`) instead of design tokens | High — instantly off-brand on theme changes | `var(--p-color-bg-fill-success)` or a `tone` prop |\n| 4 | Modal for non-blocking errors or confirmations | High — blocks the merchant; high abandonment | `<Banner>` for in-context info, `<Toast>` for transient confirmations, inline error for forms |\n| 5 | Toast for errors that need explanation | High — disappears in 3s, easily missed | `<Banner tone=\"critical\">` with title + body + recovery action |\n| 6 | Using `tone=\"success\"` for neutral or informational | Medium — semantic noise | `tone=\"info\"` for info, no tone for neutral |\n| 7 | Using `tone=\"critical\"` for warnings | Medium — desensitizes the merchant to real errors | `tone=\"warning\"` for cautionary, `tone=\"critical\"` only for destructive/error |\n| 8 | Missing `<FormLayout>` around form fields | Medium — inconsistent spacing | Always wrap related fields in `<FormLayout>` inside `<Form>` |\n| 9 | Blocking validation on every keystroke | Medium — feels hostile, breaks flow | Validate on blur or on submit; clear errors on next change |\n| 10 | Missing `helpText` on non-obvious fields | Medium — drives support tickets | `helpText` describing format, examples, or constraints |\n| 11 | Forgetting to import `@shopify/polaris/build/esm/styles.css` | High — entire app looks unstyled | Import once in the app root, before `<AppProvider>` |\n| 12 | More than 2 filled/shaped buttons in one Card | Medium — destroys hierarchy | One primary, one secondary, rest as plain or inside `<ActionList>` |\n| 13 | `<IndexTable>` on mobile without responsive plan | Medium — horizontal scroll hell on phones | Hide columns or switch to `<ResourceList>` below the mobile breakpoint |\n| 14 | Modal sized larger than the mobile viewport | Medium — content clipped on phones | `size=\"small\"` for mobile flows; never assume desktop |\n| 15 | Icon-only buttons missing `accessibilityLabel` | Medium — screen readers see nothing | Always pass `accessibilityLabel` to icon-only `<Button>` |\n| 16 | Error states relying on color alone | Medium — fails WCAG, fails colorblind users | Pair red border with text error message and icon |\n| 17 | Importing old icon names (`DeleteMinor`, `EditMajor`) | Medium — works on v10–11, gone in v12+ | Use new icon names (`DeleteIcon`, `EditIcon`) |\n| 18 | Custom focus styles that override Polaris focus rings | Medium — keyboard users get lost | Leave focus rings alone, or use `--p-focused` tokens |\n| 19 | Card with no padding wrapper | Low — content touches Card edge | `<Box padding=\"400\">` inside, or use `<Card padding=\"400\">` (v12+) |\n| 20 | Mixing `tone=\"subdued\"` with low-contrast text on top | Low — fails AA contrast | Don't stack subdued tones; use one level of de-emphasis |\n\n---\n\n## 3. Layout anti-patterns (deepest dive)\n\n### 3.1. The deprecated Stack family\n\nIn Polaris 10 there was `<Stack>` with `vertical` and `distribution` props. Polaris 11 split it into `<VerticalStack>` and `<HorizontalStack>`. Polaris 12 renamed those to `<BlockStack>` and `<InlineStack>` to match CSS logical-property language (block axis = vertical, inline axis = horizontal).\n\nAnything still using the old names is either dead code or a build error on v12.\n\n**Wrong (Polaris 10):**\n```tsx\n<Stack vertical spacing=\"loose\">\n  <TextField label=\"Name\" value={name} onChange={setName} />\n  <TextField label=\"SKU\" value={sku} onChange={setSku} />\n</Stack>\n```\n\n**Wrong (Polaris 11):**\n```tsx\n<VerticalStack gap=\"4\">\n  <TextField label=\"Name\" value={name} onChange={setName} />\n</VerticalStack>\n```\n\n**Right (Polaris 12+):**\n```tsx\n<BlockStack gap=\"400\">\n  <TextField label=\"Name\" value={name} onChange={setName} />\n  <TextField label=\"SKU\" value={sku} onChange={setSku} />\n</BlockStack>\n```\n\nNote the gap scale also changed: `\"loose\"` / `\"4\"` became `\"400\"` (the numeric token scale). Migrator handles this:\n\n```bash\nnpx @shopify/polaris-migrator react-rename-component@v12 \\\n  --renameFrom=VerticalStack --renameTo=BlockStack \\\n  \"src/**/*.tsx\"\n```\n\n### 3.2. Modal overuse\n\nShopify's own guidance: modals block the merchant until they make a decision. They are a power move. Reach for them only when:\n\n1. The action is consequential and reversible needs explicit consent (delete, publish, send invoice)\n2. You need a focused mini-form that does not fit in the existing page\n3. You are showing a confirmation step that genuinely benefits from an interruption\n\nDo **not** use modals for:\n\n- Telling a merchant that something went wrong (use `<Banner>` in-context)\n- Confirming a successful save (use `<Toast>`)\n- Showing extra information the merchant can read later (use `<Popover>` or expand a section)\n- \"Are you sure?\" on non-destructive actions (just do the action; offer undo via Toast)\n\n### 3.3. Oversized cards\n\nA common mistake: stuffing an entire feature into a single Card with deeply nested sections. This breaks visual hierarchy and forces merchants to scroll past unrelated content.\n\n**Wrong:**\n```tsx\n<Card>\n  <Card.Section title=\"Basic info\">...</Card.Section>\n  <Card.Section title=\"Pricing\">...</Card.Section>\n  <Card.Section title=\"Inventory\">...</Card.Section>\n  <Card.Section title=\"Shipping\">...</Card.Section>\n  <Card.Section title=\"SEO\">...</Card.Section>\n  <Card.Section title=\"Advanced\">...</Card.Section>\n</Card>\n```\n\n**Right:**\n```tsx\n<BlockStack gap=\"400\">\n  <Card>\n    <BlockStack gap=\"300\">\n      <Text variant=\"headingMd\" as=\"h2\">Basic info</Text>\n      {/* fields */}\n    </BlockStack>\n  </Card>\n  <Card>\n    <BlockStack gap=\"300\">\n      <Text variant=\"headingMd\" as=\"h2\">Pricing</Text>\n      {/* fields */}\n    </BlockStack>\n  </Card>\n  {/* one Card per logical section */}\n</BlockStack>\n```\n\nRule of thumb: a Card is a unit of meaning. If two sections do not belong on the same screen for the same task, they should be separate Cards. If a Card has more than 3 sections, split it.\n\n---\n\n## 4. Form anti-patterns\n\n### 4.1. Wrong field types\n\nShopify admin merchants type tens of fields per day. Wrong field types cost them speed and trust.\n\n| Wrong | Right | Why |\n|---|---|---|\n| `<TextField>` for currency without `prefix=\"$\"` and `type=\"number\"` | `<TextField type=\"currency\" prefix=\"$\">` | Mobile keyboard, alignment, parsing |\n| `<TextField>` for a fixed choice | `<Select>` or `<ChoiceList>` | Free-text invites errors |\n| `<Select>` with 2 options | `<Checkbox>` or `<RadioButton>` | Two-state UI should not require a dropdown |\n| `<Checkbox>` for \"either A or B\" | `<RadioButton>` group | Checkbox implies independent toggles |\n| Inline `<input>` mixed with Polaris fields | `<TextField>` | Inconsistent focus rings, sizing, error states |\n\n### 4.2. Blocking validation\n\n**Wrong (validates on every keystroke):**\n```tsx\nconst handleChange = (value: string) => {\n  setName(value);\n  if (value.length < 3) {\n    setNameError(\"Must be at least 3 characters\");\n  }\n};\n```\n\nThe merchant types \"Ab\" and immediately sees an error before they can type the third letter. This is hostile.\n\n**Right (validates on blur, clears on change):**\n```tsx\nconst handleChange = (value: string) => {\n  setName(value);\n  if (nameError) setNameError(\"\"); // clear on next change\n};\n\nconst handleBlur = () => {\n  if (name.length > 0 && name.length < 3) {\n    setNameError(\"Must be at least 3 characters\");\n  }\n};\n\n<TextField\n  label=\"Name\"\n  value={name}\n  onChange={handleChange}\n  onBlur={handleBlur}\n  error={nameError}\n/>\n```\n\nFor form-level validation (required fields, cross-field rules) — validate on submit, surface all errors via `<Banner tone=\"critical\">` at the top of the form **plus** inline `error` props on each field.\n\n### 4.3. Missing FormLayout\n\n`<FormLayout>` enforces the correct vertical rhythm and field grouping. Without it, fields touch each other or float with wrong spacing.\n\n**Wrong:**\n```tsx\n<Form onSubmit={handleSubmit}>\n  <TextField label=\"Name\" value={name} onChange={setName} />\n  <TextField label=\"Email\" value={email} onChange={setEmail} />\n  <Button submit>Save</Button>\n</Form>\n```\n\n**Right:**\n```tsx\n<Form onSubmit={handleSubmit}>\n  <FormLayout>\n    <TextField label=\"Name\" value={name} onChange={setName} />\n    <TextField label=\"Email\" value={email} onChange={setEmail} />\n  </FormLayout>\n  <Button submit>Save</Button>\n</Form>\n```\n\nAlso use `<FormLayout.Group>` to put related short fields on the same row (first name + last name, city + zip).\n\n---\n\n## 5. Color and tone anti-patterns\n\nPolaris uses semantic `tone` props rather than direct colors. Four tones cover almost everything:\n\n| Tone | Meaning | Use for |\n|---|---|---|\n| `success` | Positive completion | Order shipped, settings saved, payment received |\n| `critical` | Destructive or error | Delete, payment failed, validation error |\n| `warning` | Cautionary, reversible | Low stock, plan limit, draft will be lost |\n| `info` | Neutral information | New feature notice, helpful tip, ongoing sync |\n\nPlus tone modifiers on text: `tone=\"subdued\"` for de-emphasized copy.\n\n**Common misuses:**\n\n```tsx\n// WRONG — success tone for a neutral confirmation\n<Banner tone=\"success\">Your settings are saved automatically.</Banner>\n\n// RIGHT\n<Banner tone=\"info\">Your settings are saved automatically.</Banner>\n```\n\n```tsx\n// WRONG — critical for a warning\n<Banner tone=\"critical\">You have 3 days left in your trial.</Banner>\n\n// RIGHT\n<Banner tone=\"warning\">You have 3 days left in your trial.</Banner>\n```\n\n```tsx\n// WRONG — no tone, custom red color\n<Banner style={{ borderColor: \"red\" }}>Action required</Banner>\n\n// RIGHT\n<Banner tone=\"warning\" title=\"Action required\">...</Banner>\n```\n\nRule: if you reach for a hex code in a Polaris context, you are probably picking the wrong tone instead.\n\n---\n\n## 6. Custom CSS overrides — when prohibited\n\nPolaris components are designed as a closed contract. Most do **not** accept a `className` prop. Even when you can shim CSS in via wrappers, doing so is an anti-pattern.\n\n**Hard no:**\n- Overriding internal selectors via `.Polaris-Button { ... }` in global CSS\n- Wrapping in a `<div className=\"my-custom-styles\">` and targeting child Polaris classes\n- Using `!important` to win against Polaris styles\n- Patching at runtime via `style={{ }}` on Polaris components that do not document a `style` prop\n\n**Acceptable:**\n- Using `<Box>` with token props (`padding`, `background`, `borderRadius`, `borderColor`) to wrap Polaris content\n- Adding custom CSS to your own non-Polaris components, using `var(--p-*)` tokens\n- Using documented props (`tone`, `variant`, `size`, `gap`)\n- Wrapping the entire app shell in a layout div with custom CSS (outermost only)\n\n**The decision rule:** if you want to change how a Polaris component looks, your three options are:\n\n1. Use a documented prop (`tone`, `variant`, `size`).\n2. Wrap with `<Box>` and adjust spacing/background.\n3. Build a custom component using `<Box>` + Polaris tokens and stop using the Polaris component for that case.\n\nThere is no fourth option. Reaching past the contract via CSS will break on every Polaris minor release.\n\n---\n\n## 7. Off-brand styles — use design tokens\n\nPolaris exposes its design tokens as CSS custom properties prefixed `--p-`. They are the single source of truth for color, spacing, radius, shadow, type, motion.\n\n### 7.1. Most-used token families\n\n| Family | Examples |\n|---|---|\n| Color — surface | `--p-color-bg-surface`, `--p-color-bg-surface-secondary`, `--p-color-bg-surface-hover` |\n| Color — fill | `--p-color-bg-fill`, `--p-color-bg-fill-success`, `--p-color-bg-fill-warning`, `--p-color-bg-fill-critical` |\n| Color — text | `--p-color-text`, `--p-color-text-subdued`, `--p-color-text-success`, `--p-color-text-critical` |\n| Color — border | `--p-color-border`, `--p-color-border-subdued`, `--p-color-border-focused` |\n| Spacing | `--p-space-100` (4px), `--p-space-200` (8px), `--p-space-300` (12px), `--p-space-400` (16px), `--p-space-500` (20px), `--p-space-600` (24px), `--p-space-800` (32px) |\n| Border radius | `--p-border-radius-100`, `--p-border-radius-200`, `--p-border-radius-300` |\n| Shadow | `--p-shadow-100`, `--p-shadow-200`, `--p-shadow-300` |\n| Typography | `--p-font-size-300`, `--p-font-weight-medium`, `--p-font-line-height-500` |\n\n### 7.2. Off-brand examples\n\n```css\n/* WRONG — hardcoded brand color, breaks if Shopify rebrands or merchant theme changes */\n.my-success-pill {\n  background: #008060;\n  color: white;\n  padding: 8px 12px;\n  border-radius: 4px;\n}\n\n/* RIGHT — uses Polaris tokens, follows admin theme automatically */\n.my-success-pill {\n  background: var(--p-color-bg-fill-success);\n  color: var(--p-color-text-on-color);\n  padding: var(--p-space-200) var(--p-space-300);\n  border-radius: var(--p-border-radius-200);\n}\n```\n\n**Even better:** don't write the CSS at all. Use `<Badge tone=\"success\">Active</Badge>`.\n\nAlways prefer **semantic** tokens (`--p-color-bg-fill-success`) over **primitive** tokens (`--p-color-green-500`). Primitive tokens may shift; semantic tokens hold their meaning across themes and versions.\n\n---\n\n## 8. Mobile responsive failures\n\nShopify admin is used on mobile by ~40% of merchants on certain workflows (order check, inventory tweak, support reply). Embedded apps that assume desktop fail in production.\n\n### 8.1. IndexTable on narrow viewports\n\n`<IndexTable>` is wide by default. On a 375px viewport, anything past 3 columns scrolls horizontally inside an iframe — confusing and easy to miss.\n\n**Fixes, ranked:**\n\n1. **Hide columns below a breakpoint.** Polaris does not have a built-in `hideOnMobile`; use a conditional render based on viewport width.\n   ```tsx\n   const isMobile = useMediaQuery(\"(max-width: 768px)\");\n\n   const headings = isMobile\n     ? [{ title: \"Name\" }, { title: \"Status\" }]\n     : [\n         { title: \"Name\" },\n         { title: \"SKU\" },\n         { title: \"Inventory\" },\n         { title: \"Status\" },\n         { title: \"Last updated\" },\n       ];\n   ```\n2. **Switch to `<ResourceList>` for mobile.** Lists stack naturally; tables do not.\n3. **Pin the most important column.** Polaris IndexTable supports sticky columns since v12.\n\n### 8.2. Modal sizing\n\n```tsx\n// WRONG — assumes desktop; clips on phones\n<Modal open={open} onClose={close} title=\"Edit\" size=\"large\">\n\n// RIGHT — small modal on mobile, large on desktop\n<Modal\n  open={open}\n  onClose={close}\n  title=\"Edit\"\n  size={isMobile ? \"small\" : \"large\"}\n>\n```\n\n### 8.3. Card padding on small screens\n\nGenerous desktop padding (e.g., `padding=\"500\"`) eats the small screen. Use responsive padding:\n\n```tsx\n<Card padding={{ xs: \"300\", md: \"500\" }}>\n  ...\n</Card>\n```\n\n### 8.4. Layout sections\n\nTwo-column layouts must collapse to one column under ~768px. `<Layout>` and `<InlineGrid>` handle this automatically only if you use the responsive `columns` prop:\n\n```tsx\n// WRONG — always two columns, broken on mobile\n<InlineGrid columns=\"1fr 1fr\" gap=\"400\">\n\n// RIGHT — collapses to one column on mobile\n<InlineGrid columns={{ xs: 1, md: 2 }} gap=\"400\">\n```\n\n---\n\n## 9. Accessibility failures inside Polaris\n\nPolaris is WCAG 2.1 AA compliant out of the box. App teams break that compliance with custom code around Polaris.\n\n### 9.1. Missing labels\n\n```tsx\n// WRONG — no label, no accessibility label\n<TextField placeholder=\"Search...\" value={query} onChange={setQuery} />\n\n// RIGHT\n<TextField\n  label=\"Search products\"\n  labelHidden\n  placeholder=\"Search...\"\n  value={query}\n  onChange={setQuery}\n/>\n```\n\n`labelHidden` keeps the visual layout clean while still providing an accessible label to screen readers.\n\n### 9.2. Icon-only buttons\n\n```tsx\n// WRONG — screen reader hears \"Button\"\n<Button icon={DeleteIcon} onClick={handleDelete} />\n\n// RIGHT\n<Button\n  icon={DeleteIcon}\n  accessibilityLabel=\"Delete product\"\n  onClick={handleDelete}\n/>\n```\n\n### 9.3. Focus order\n\nWhen you build a modal or popover, the first focusable element should be the most likely action. Polaris handles this for `<Modal>` (first input or primary action gets focus). But if you build a custom flow:\n\n- First focusable on open = main task action or first input\n- Tab should move forward through fields in reading order\n- Escape closes the modal\n- Focus returns to the trigger element on close\n\nIf you implement a custom dropdown or popover, replicate this. Better: just use `<Popover>` and `<ActionList>`.\n\n### 9.4. Contrast\n\nPolaris tokens are designed to pass AA. Two common ways teams break contrast:\n\n1. **Stacking subdued tones.** `<Text tone=\"subdued\">` on `<Box background=\"bg-surface-secondary\">` can drop below 4.5:1.\n2. **Custom colors over branded backgrounds.** If you change a Card background via `<Box background=\"...\">`, re-test all text inside it.\n\nRun an Axe audit before submission. The most common embedded-app failures are: missing form labels, icon-only buttons without aria-label, and color-only error indication.\n\n---\n\n## 10. Migration drift — Polaris 10 → 11 → 12+\n\nIf your codebase has been around for more than 18 months, you have drift. Symptoms: components that worked have started warning, build size grew, some screens use new patterns and some use old.\n\n### 10.1. Component renames (10 → 11 → 12)\n\n| Polaris 10 | Polaris 11 | Polaris 12+ |\n|---|---|---|\n| `<Stack vertical>` | `<VerticalStack>` | `<BlockStack>` |\n| `<Stack>` (horizontal) | `<HorizontalStack>` | `<InlineStack>` |\n| `<Stack.Item>` | (n/a — children are direct) | (n/a) |\n| `<TextStyle variation=\"strong\">` | `<Text fontWeight=\"semibold\">` | `<Text fontWeight=\"semibold\">` |\n| `<DisplayText>` | `<Text variant=\"headingXl\">` | `<Text variant=\"headingXl\">` |\n| `<Heading>` | `<Text variant=\"headingMd\">` | `<Text variant=\"headingMd\">` |\n| `<Subheading>` | `<Text variant=\"headingSm\">` | `<Text variant=\"headingSm\">` |\n| `<Caption>` | `<Text variant=\"bodySm\">` | `<Text variant=\"bodySm\">` |\n| `<Visually Hidden>` | `<VisuallyHidden>` | (use `labelHidden` or `Text visuallyHidden`) |\n| `<Card sectioned>` | `<Card><Card.Section>` | `<Card padding=\"400\">` |\n| `<Stack spacing=\"loose\">` | `gap=\"4\"` | `gap=\"400\"` |\n\n### 10.2. Icon renames\n\nOld: `DeleteMinor`, `EditMajor`, `SaveMinor`, `SearchMinor`, `CancelMinor`, `PlusMinor`, `MinusMinor`, `RefreshMinor`, `ChevronDownMinor`.\n\nNew (v12+): `DeleteIcon`, `EditIcon`, `SaveIcon`, `SearchIcon`, `XIcon`, `PlusIcon`, `MinusIcon`, `RefreshIcon`, `ChevronDownIcon`.\n\nThe \"Minor / Major\" suffix is gone. Always `*Icon`.\n\n```tsx\n// WRONG (Polaris 10–11)\nimport { DeleteMinor, EditMajor } from \"@shopify/polaris-icons\";\n\n// RIGHT (Polaris 12+)\nimport { DeleteIcon, EditIcon } from \"@shopify/polaris-icons\";\n```\n\n### 10.3. Use the migrator\n\n```bash\n# Install once\nnpm install --save-dev @shopify/polaris-migrator\n\n# Run all v11 → v12 migrations\nnpx @shopify/polaris-migrator migrate v11-react-rename-components \"src/**/*.tsx\"\nnpx @shopify/polaris-migrator migrate v12-react-replace-icons \"src/**/*.tsx\"\nnpx @shopify/polaris-migrator migrate v12-react-update-spacing-tokens \"src/**/*.tsx\"\n```\n\nAfter running, do a manual audit of:\n- `<Card>` instances (sectioned → padding)\n- Custom CSS using old spacing names\n- Storybook stories\n- Snapshot tests\n\n---\n\n## 11. Anti-pattern → fix table with before/after code\n\n### 11.1. Deprecated Stack\n\n```tsx\n// BEFORE\n<Stack vertical spacing=\"tight\">\n  <Stack.Item><TextField label=\"Name\" /></Stack.Item>\n  <Stack.Item><TextField label=\"SKU\" /></Stack.Item>\n</Stack>\n\n// AFTER\n<BlockStack gap=\"200\">\n  <TextField label=\"Name\" />\n  <TextField label=\"SKU\" />\n</BlockStack>\n```\n\n### 11.2. Modal overuse for confirmation\n\n```tsx\n// BEFORE — interrupts the merchant\n<Modal\n  open={savedOpen}\n  onClose={() => setSavedOpen(false)}\n  title=\"Saved!\"\n  primaryAction={{ content: \"OK\", onAction: () => setSavedOpen(false) }}\n>\n  <Modal.Section>Your changes are saved.</Modal.Section>\n</Modal>\n\n// AFTER — Toast for transient success\nshopify.toast.show(\"Changes saved\", { duration: 3000 });\n// or in React:\n<Toast content=\"Changes saved\" onDismiss={dismiss} />\n```\n\n### 11.3. Toast for an error that needs detail\n\n```tsx\n// BEFORE — disappears in 3s, no recovery path\n<Toast content=\"Failed\" error onDismiss={dismiss} />\n\n// AFTER — Banner with title, body, and recovery action\n<Banner\n  tone=\"critical\"\n  title=\"Could not save product\"\n  action={{ content: \"Retry\", onAction: handleRetry }}\n>\n  <p>The inventory API timed out. Your changes are not saved.</p>\n</Banner>\n```\n\n### 11.4. Wrong tone\n\n```tsx\n// BEFORE\n<Banner tone=\"success\">You have 3 days left in your trial.</Banner>\n\n// AFTER\n<Banner tone=\"warning\" title=\"Trial ending soon\">\n  You have 3 days left in your trial.\n</Banner>\n```\n\n### 11.5. Hex colors\n\n```tsx\n// BEFORE\n<div style={{ backgroundColor: \"#d3f9d8\", padding: \"12px\" }}>\n  Connected\n</div>\n\n// AFTER (option A — use the component)\n<Badge tone=\"success\">Connected</Badge>\n\n// AFTER (option B — use Box with tokens)\n<Box background=\"bg-fill-success\" padding=\"300\">\n  <Text tone=\"success\">Connected</Text>\n</Box>\n```\n\n### 11.6. className override\n\n```tsx\n// BEFORE — does nothing on most Polaris components; relies on fragile selectors\n<Button className=\"my-custom-button\">Save</Button>\n// in CSS:\n.my-custom-button { background: orange !important; }\n\n// AFTER — use documented props\n<Button variant=\"primary\" tone=\"success\">Save</Button>\n```\n\n### 11.7. No FormLayout\n\n```tsx\n// BEFORE\n<Form onSubmit={submit}>\n  <TextField label=\"Name\" value={name} onChange={setName} />\n  <TextField label=\"Email\" value={email} onChange={setEmail} />\n  <Checkbox label=\"Subscribe\" checked={sub} onChange={setSub} />\n  <Button submit>Save</Button>\n</Form>\n\n// AFTER\n<Form onSubmit={submit}>\n  <FormLayout>\n    <TextField label=\"Name\" value={name} onChange={setName} />\n    <TextField label=\"Email\" value={email} onChange={setEmail} />\n    <Checkbox label=\"Subscribe\" checked={sub} onChange={setSub} />\n  </FormLayout>\n  <Box paddingBlockStart=\"400\">\n    <Button submit variant=\"primary\">Save</Button>\n  </Box>\n</Form>\n```\n\n### 11.8. Blocking keystroke validation\n\n```tsx\n// BEFORE\n<TextField\n  label=\"Name\"\n  value={name}\n  onChange={(v) => {\n    setName(v);\n    setError(v.length < 3 ? \"Too short\" : \"\");\n  }}\n  error={error}\n/>\n\n// AFTER\n<TextField\n  label=\"Name\"\n  value={name}\n  onChange={(v) => { setName(v); if (error) setError(\"\"); }}\n  onBlur={() => {\n    if (name.length > 0 && name.length < 3) setError(\"Must be at least 3 characters\");\n  }}\n  error={error}\n  helpText=\"At least 3 characters\"\n/>\n```\n\n### 11.9. Old icon names\n\n```tsx\n// BEFORE\nimport { DeleteMinor, EditMajor, SaveMinor } from \"@shopify/polaris-icons\";\n\n// AFTER\nimport { DeleteIcon, EditIcon, SaveIcon } from \"@shopify/polaris-icons\";\n```\n\n### 11.10. IndexTable on mobile\n\n```tsx\n// BEFORE — 6 columns, horizontal scroll on mobile\n<IndexTable\n  headings={[\n    { title: \"Name\" }, { title: \"SKU\" }, { title: \"Vendor\" },\n    { title: \"Inventory\" }, { title: \"Price\" }, { title: \"Status\" }\n  ]}\n  ...\n/>\n\n// AFTER — collapse to 2 columns under md\nconst isMobile = useBreakpoints().smDown;\n<IndexTable\n  headings={\n    isMobile\n      ? [{ title: \"Name\" }, { title: \"Status\" }]\n      : [\n          { title: \"Name\" }, { title: \"SKU\" }, { title: \"Vendor\" },\n          { title: \"Inventory\" }, { title: \"Price\" }, { title: \"Status\" }\n        ]\n  }\n  ...\n/>\n```\n\n---\n\n## 12. Decision tree\n\nUse this when in doubt about which component to pick.\n\n**Need to tell the merchant something happened.**\n\n- Was the merchant's action successful, and they do not need to think about it further? → `<Toast>`\n- Did something fail or need attention that the merchant should see, in context, but can still keep working? → `<Banner>` (use the right tone)\n- Does the merchant need to confirm or make a decision before continuing? → `<Modal>`\n- Is it a form field problem? → inline `error` prop on the field + `<Banner tone=\"critical\">` summary at top of form\n\n**Need to lay something out.**\n\n- Vertical stack of items? → `<BlockStack gap=\"...\">`\n- Horizontal row of items? → `<InlineStack gap=\"...\">`\n- Grid of items, responsive? → `<InlineGrid columns=\"...\" gap=\"...\">`\n- Wrapper with padding / background / border? → `<Box ...>`\n- A full page section with title and actions? → `<Card>` containing `<BlockStack>` containing `<Text variant=\"headingMd\">` + body\n\n**Need to show data.**\n\n- Tabular, with sorting / selection / bulk actions? → `<IndexTable>`\n- Browseable list with custom item rendering? → `<ResourceList>`\n- Static key-value pairs? → `<DescriptionList>` or `<BlockStack>` with labeled rows\n\n**Need to collect input.**\n\n- Single line of text? → `<TextField>`\n- Multi-line text? → `<TextField multiline={4}>`\n- Pick one of many? → `<Select>` (5+ options) or `<ChoiceList>` (2–4 options, visible)\n- Toggle on/off? → `<Checkbox>` (independent) or `<RadioButton>` (mutually exclusive)\n- Date? → `<DatePicker>`\n- Wrap fields with consistent spacing? → `<FormLayout>` inside `<Form>`\n\n**Need to style something custom.**\n\n- Can I do it with a Polaris prop? → use the prop\n- Can I do it with `<Box>` and tokens? → use `<Box>`\n- Neither? → build a custom component using `var(--p-*)` tokens; do not override Polaris CSS\n\n---\n\n## 13. Code review checklist (20 items)\n\nWalk through this list on every PR that touches a Polaris file. Each item maps to one of the anti-patterns above.\n\n- [ ] **1. No deprecated Stack.** No imports of `Stack`, `LegacyStack`, `VerticalStack`, `HorizontalStack`. Only `BlockStack` and `InlineStack`.\n- [ ] **2. No className on Polaris components.** Custom styling uses `<Box>` + token props, not class overrides.\n- [ ] **3. No hex codes.** All colors come from `var(--p-color-*)` tokens or `tone` props.\n- [ ] **4. Modals only for blocking decisions.** No modals for success confirmations or non-blocking errors.\n- [ ] **5. Toasts only for transient success.** No toasts for errors that need explanation or recovery.\n- [ ] **6. Tone semantics are correct.** Success = positive, critical = destructive/error, warning = cautionary, info = neutral.\n- [ ] **7. Forms use `<FormLayout>`.** Fields inside `<Form>` are wrapped in `<FormLayout>` for consistent spacing.\n- [ ] **8. Validation is not on keystroke.** Errors appear on blur or submit, not as the merchant is typing.\n- [ ] **9. `helpText` exists for non-obvious fields.** Format, examples, or constraints are explained inline.\n- [ ] **10. Polaris CSS is imported at app root.** `import \"@shopify/polaris/build/esm/styles.css\"` runs before `<AppProvider>`.\n- [ ] **11. Max 2 filled buttons per Card.** Extras move to `<ActionList>` or plain buttons.\n- [ ] **12. IndexTable hides columns on mobile.** Column set changes via `useBreakpoints` or media query.\n- [ ] **13. Modal `size` is responsive.** Small on mobile, medium/large on desktop.\n- [ ] **14. Icon-only buttons have `accessibilityLabel`.** No bare `<Button icon={...} />` without a label.\n- [ ] **15. Errors do not rely on color alone.** Red border + text message + (optional) icon.\n- [ ] **16. Icon names are the new style.** `DeleteIcon` not `DeleteMinor`. No `*Minor` or `*Major` suffix.\n- [ ] **17. No overrides of Polaris focus rings.** Focus styling is left alone or uses `--p-focused` tokens.\n- [ ] **18. Cards have padding.** Either `<Card padding=\"400\">` (v12+) or wrapped in `<Box padding=\"400\">`.\n- [ ] **19. Subdued tones are not stacked.** `tone=\"subdued\"` text is not on subdued surface without contrast check.\n- [ ] **20. AppProvider wraps the tree.** Exactly one `<AppProvider i18n={...}>` at the root.\n\n---\n\n## Quick reference — token cheat sheet\n\n```css\n/* Spacing — use on padding, gap, margin */\n--p-space-100   /* 4px  */\n--p-space-200   /* 8px  */\n--p-space-300   /* 12px */\n--p-space-400   /* 16px */\n--p-space-500   /* 20px */\n--p-space-600   /* 24px */\n--p-space-800   /* 32px */\n\n/* Color — semantic */\n--p-color-bg-surface\n--p-color-bg-surface-secondary\n--p-color-bg-fill-success\n--p-color-bg-fill-warning\n--p-color-bg-fill-critical\n--p-color-text\n--p-color-text-subdued\n--p-color-text-success\n--p-color-text-critical\n--p-color-border\n--p-color-border-focused\n\n/* Radius */\n--p-border-radius-100  /* 4px  */\n--p-border-radius-200  /* 8px  */\n--p-border-radius-300  /* 12px */\n\n/* Shadow */\n--p-shadow-100\n--p-shadow-200\n--p-shadow-300\n\n/* Typography */\n--p-font-size-300\n--p-font-weight-medium\n--p-font-line-height-500\n```\n\nWhen in doubt: use the component prop. When no prop fits: use `<Box>` with token props. When `<Box>` does not fit: write CSS using `var(--p-*)` tokens. Never reach past these three layers.\n\n---\n\n## Sources\n\n- [Migrating from v11 to v12 — Shopify Polaris React](https://polaris-react.shopify.com/version-guides/migrating-from-v11-to-v12)\n- [Block stack — Shopify Polaris React](https://polaris-react.shopify.com/components/layout-and-structure/block-stack)\n- [Inline stack — Shopify Polaris React](https://polaris-react.shopify.com/components/layout-and-structure/inline-stack)\n- [Legacy stack — Shopify Polaris React](https://polaris-react.shopify.com/components/deprecated/legacy-stack)\n- [Error messages — Shopify Polaris](https://legacy.polaris.shopify.com/patterns/error-messages)\n- [Common actions — Shopify Polaris](https://polaris.shopify.com/patterns/common-actions/best-practices)\n- [Tokens — Shopify Polaris React](https://polaris-react.shopify.com/design/layout/layout-tokens)\n- [Color tokens — Shopify Polaris React](https://polaris-react.shopify.com/design/colors/color-tokens)\n- [polaris-tokens — GitHub](https://github.com/Shopify/polaris-tokens)\n- [Shopify Polaris App Design: Build Review-Ready Apps — grumspot](https://grumspot.com/blog/shopify-polaris-app-design)\n"},"changes":[],"summary":"First saved snapshot. No earlier version is available for comparison.","summary_kind":"deterministic","summary_metadata":{}}