| name | polaris-design |
| description | Design and implement UI components and pages using Shopify Polaris in a React app. Guide layout, forms, data tables, feedback states, modals, and navigation following idiomatic Polaris patterns. Reference docs at https://polaris-react.shopify.com/components |
| argument-hint | [describe the UI component or page to build] |
| disable-model-invocation | true |
| allowed-tools | Read, Write, Edit, Glob, Grep |
Polaris Design Skill
You are a Polaris UI expert. When implementing UI components or pages, follow idiomatic Polaris patterns and the conventions of the codebase you're working in. Reference https://polaris-react.shopify.com/components for official Polaris docs.
Below are idiomatic Polaris patterns for common UI — layout, forms, data tables,
feedback states, modals, and navigation. They're framework-generic; adapt file
placement and naming to your own project's conventions.
Project Structure (adapt to your repo)
A typical Polaris React app organizes UI roughly like this — map these to your repo:
- Pages:
<frontend>/pages/
- Common components:
<frontend>/components/common/
- Shared components:
<frontend>/components/
- Feature modules:
<frontend>/modules/
- Hooks:
<frontend>/hooks/
- Skeleton loaders:
<frontend>/components/loader/
Step 1: Understand the Request
Parse $ARGUMENTS to determine the UI task:
| Task | Examples |
|---|
| New page | "create a page for X", "add route for X" |
| New component | "create a card/table/form for X" |
| Modify existing | "add a field to X", "update layout of X" |
| Data table | "list of X with filters/search" |
| Form | "settings form for X", "edit form for X" |
| Modal | "confirmation modal", "detail modal for X" |
Before writing code, read the most relevant existing file to understand the surrounding context.
Step 2: Identify the Right Pattern
Use the pattern reference below. Always read a real example file before implementing.
Pattern Reference
Layout Patterns
Standard Page (most pages)
<Page
title="Page Title"
backAction={{ onAction: () => navigate("/back") }}
primaryAction={<Button variant="primary">Save</Button>}
>
<BlockStack gap="400">
<Grid>
<Grid.Cell columnSpan={{ xs: 6, sm: 6, md: 4, lg: 8, xl: 8 }}>{/* Main content */}</Grid.Cell>
<Grid.Cell columnSpan={{ xs: 6, sm: 6, md: 2, lg: 4, xl: 4 }}>{/* Sidebar */}</Grid.Cell>
</Grid>
</BlockStack>
</Page>
Full-width Page (lists, tables)
<Page title="Products">
<BlockStack gap="400">
<Card padding="0">
<IndexTable ... />
</Card>
</BlockStack>
</Page>
Dashboard Grid (home/overview pages)
<InlineGrid
columns={{ xs: 1, sm: 1, md: 2, lg: 2, xl: 2 }}
gap="400"
>
<CardA />
<CardB />
</InlineGrid>
Settings Page
<BlockStack gap="400">
<Card>
<BlockStack gap="400">
<Text variant="headingMd" as="h5">Section Title</Text>
<Grid>
<Grid.Cell columnSpan={{ xs: 6, sm: 3, md: 3, lg: 6, xl: 6 }}>
<TextField label="Field" ... />
</Grid.Cell>
</Grid>
</BlockStack>
</Card>
</BlockStack>
Card Patterns
Basic Card
<Card>
<BlockStack gap="400">
<Text
variant="headingMd"
as="h5"
>
Title
</Text>
{/* content */}
</BlockStack>
</Card>
Toggle / Status Card
<ToggleCard
label="Feature Name"
badgeLabel="Active"
color="success"
btnText="Disable"
onButtonClick={handleToggle}
content="Description of what this feature does."
/>
Card with no padding (for IndexTable)
<Card padding="0">
<IndexTable ... />
</Card>
Form Patterns
Text Field
import { TextField } from "@shopify/polaris";
<TextField
label={t("Field Label")}
value={value}
onChange={setValue}
autoComplete="off"
helpText={t("Optional helper text")}
error={error}
/>;
Inline Fields (Grid-based)
<Grid>
<Grid.Cell columnSpan={{ xs: 6, sm: 3, md: 3, lg: 6, xl: 6 }}>
<TextField label={t("First Name")} ... />
</Grid.Cell>
<Grid.Cell columnSpan={{ xs: 6, sm: 3, md: 3, lg: 6, xl: 6 }}>
<TextField label={t("Last Name")} ... />
</Grid.Cell>
</Grid>
Custom Select (Popover + Listbox)
<SelectInput
title="Language"
options={[{ label: "English", value: "en" }]}
selected={selected}
onChange={setSelected}
/>
Form Actions (Save/Discard)
<ContextualSaveBar
id="unique-form-id"
open={formState.isDirty}
isLoading={isSaving}
onSave={handleSubmit}
onDiscard={() => reset()}
/>
Data Table Patterns
IndexTable with Filters (standard list page)
<Card padding="0">
<IndexFilters
queryValue={search}
onQueryChange={setSearch}
onQueryClear={() => setSearch("")}
tabs={tabs}
selected={tabIndex}
filters={filters}
appliedFilters={appliedFilters}
mode={mode}
setMode={setMode}
cancelAction={{ onAction: handleCancel }}
canCreateNewView={false}
/>
<IndexTable
resourceName={{ singular: "item", plural: "items" }}
itemCount={items.length}
selectedItemsCount={allSelected ? "all" : selected.length}
onSelectionChange={handleSelectionChange}
headings={[{ title: t("Name") }, { title: t("Status") }, { title: t("Action") }]}
emptyState={<TableEmptyState />}
>
{items.map((item, index) => (
<IndexTable.Row
id={item.id}
key={item.id}
position={index}
selected={selected.includes(item.id)}
>
{item.name}
Active
Edit
))}
</>
Non-selectable Table (read-only data)
<Card padding="0">
<IndexTable
selectable={false}
headings={[{ title: "Column" }]}
itemCount={items.length}
>
{/* rows */}
</IndexTable>
</Card>
Feedback & State Patterns
Loading (Skeleton)
import DummyPageSkeleton from "@/components/loader/DummyPageSkeleton";
import SkeletonLoader from "@/components/loader/SkeletonLoader";
import TableRowsSkeleton from "@/components/loader/TableRowsSkeleton";
if (isLoading) return <DummyPageSkeleton />;
if (isLoading)
return (
<TableRowsSkeleton
rows={10}
columns={4}
/>
);
Banner
import { Banner } from "@shopify/polaris";
<Banner tone="warning">{t("Warning message here")}</Banner>;
Toast
shopify.toast.show(t("Saved successfully"), { duration: 3000 });
shopify.toast.show(t("Something went wrong"), { isError: true });
Badge (status indicators)
import { Badge } from "@shopify/polaris";
<Badge tone="success">Active</Badge>
<Badge tone="warning">Pending</Badge>
<Badge tone="attention">Error</Badge>
<Badge tone="info">Draft</Badge>
Empty State
<EmptyPage
image="/path/to/image.svg"
heading={t("No items found")}
content={t("Start by adding your first item.")}
primaryAction={<Button variant="primary">{t("Add Item")}</Button>}
insideCard
/>
Modal Patterns
Confirmation Modal
<ConfirmationModal
show={showModal}
setOpen={setShowModal}
title={t("Delete Item")}
content={t("Are you sure you want to delete this item? This cannot be undone.")}
primaryActionText={t("Delete")}
primaryAction={handleDelete}
primaryActionIsDestructive
loading={isDeleting}
/>
Custom Modal with Content
<Modal
type="app-bridge"
open={show}
setOpen={setShow}
variant="base"
>
<Modal.Section>
<Box padding="400">
<BlockStack gap="400">{/* modal content */}</BlockStack>
</Box>
<Modal.TitleBar title={t("Modal Title")}>
<button
variant="primary"
onClick={handleConfirm}
>
{t("Confirm")}
</button>
<button onClick={() => setShow(false)}>{t("Cancel")}</button>
</Modal.TitleBar>
</Modal.Section>
</Modal>
Navigation Patterns
Tabs
import CustomTab from "@/components/common/CustomTab";
const tabs = [
{ id: "tab-1", content: t("Tab One") },
{ id: "tab-2", content: t("Tab Two"), badge: <Badge tone="attention">3</Badge> },
];
<CustomTab
tabs={tabs}
selected={selectedTab}
onTabClick={setSelectedTab}
fitted
/>;
Pagination (inline with IndexTable)
<Page
pagination={{
hasPrevious: !!pagination?.prev,
onPrevious: () => goToPage(pagination.prev),
hasNext: !!pagination?.next,
onNext: () => goToPage(pagination.next),
}}
>
Typography Reference
import { Text } from "@shopify/polaris";
<Text variant="headingLg" as="h1">Page title (H1)</Text>
<Text variant="headingLg" as="h2">Section title (H2)</Text>
<Text variant="headingMd" as="h3">Card title (H3)</Text>
<Text variant="headingSm" as="h4">Subsection title (H4)</Text>
<Text variant="bodyLg" as="p">Large body text</Text>
Spacing Reference (gap / padding values)
| Value | Size | When to Use |
|---|
"100" | 4px | Tight inline spacing |
"200" | 8px | Between related items |
"300" | 12px | Medium spacing |
"400" | 16px | Standard section spacing (most common) |
"600" | 24px | Between major sections |
"800" | 32px | Large vertical gaps |
"1600" | 64px | Empty state padding |
Responsive Grid Reference
<Grid.Cell columnSpan={{ xs: 6, sm: 6, md: 4, lg: 8, xl: 8 }} />
<Grid.Cell columnSpan={{ xs: 6, sm: 6, md: 2, lg: 4, xl: 4 }} />
<Grid.Cell columnSpan={{ xs: 6, sm: 3, md: 3, lg: 6, xl: 6 }} />
<Grid.Cell columnSpan={{ xs: 6, sm: 2, md: 2, lg: 4, xl: 4 }} />
Rules
- Always use i18n — wrap all user-facing strings with
t("...") from react-i18next.
- Reuse before creating — first look in shared libraries (
components/common/, components/loader/), then in feature-specific components under components/ before creating a new component.
- No hardcoded spacing — always use Polaris gap/padding tokens (
"200", "400", etc.), never CSS pixels.
- Card > Box for containers — prefer
<Card> for grouped content; use <Box> only for minor padding/spacing adjustments.
- IndexTable for lists — use
<IndexTable> (not <DataTable>) for all interactive data lists.
- Card padding="0" for tables — always wrap IndexTable in
<Card padding="0">.
- ContextualSaveBar for forms — wrap dirty-state save bars in a reusable component.
- Skeleton loaders — use existing components in
components/loader/, never raw SkeletonBodyText directly in pages.
- Follow .prettierrc — double quotes, 120 print width, trailing comma es5, 2-space tabs.
- Run pre-commit — always run
pnpm run pre-commit after frontend changes.
Step 3: Implement
- Read the most relevant existing file as a reference first.
- Implement using the patterns above.
- Confirm all strings use
t("...").
- Confirm no new components duplicate existing ones in
components/common/.
- Show a summary of files created/modified.