Polaris UI Patterns Skill
Purpose
This skill provides reusable UI patterns and templates for common page layouts in Shopify apps using Polaris Web Components.
When to Use This Skill
-
Creating new pages (index, detail, form)
-
Implementing common UI patterns
-
Building consistent layouts
-
Adding empty states
-
Creating modals and forms
-
Implementing tables with actions
Core Patterns
Pattern 1: Index/List Page
Use for: Products, Orders, Customers, Custom Entities
import type { LoaderFunctionArgs } from "react-router"; import { useLoaderData } from "react-router"; import { authenticate } from "../shopify.server"; import { db } from "../db.server"; import { json } from "@remix-run/node";
export const loader = async ({ request }: LoaderFunctionArgs) => { const { session } = await authenticate.admin(request); const shop = await db.shop.findUnique({ where: { shopDomain: session.shop } });
const items = await db.item.findMany({ where: { shopId: shop.id }, orderBy: { createdAt: 'desc' }, take: 50, });
const stats = { total: items.length, active: items.filter(i => i.isActive).length, };
return json({ items, stats }); };
export default function ItemsIndexPage() { const { items, stats } = useLoaderData<typeof loader>();
return ( <s-page heading="Items"> {/* Stats Section */} <s-section> <s-grid columns="3"> <s-box border="base" borderRadius="base" padding="400"> <s-stack gap="200" direction="vertical"> <s-text variant="headingMd" as="h3">Total Items</s-text> <s-text variant="heading2xl" as="p">{stats.total}</s-text> </s-stack> </s-box> <s-box border="base" borderRadius="base" padding="400"> <s-stack gap="200" direction="vertical"> <s-text variant="headingMd" as="h3">Active</s-text> <s-text variant="heading2xl" as="p">{stats.active}</s-text> </s-stack> </s-box> </s-grid> </s-section>
{/* Table Section */}
<s-section>
<s-card>
<s-table>
<s-table-head>
<s-table-row>
<s-table-cell as="th">Name</s-table-cell>
<s-table-cell as="th">Status</s-table-cell>
<s-table-cell as="th">Created</s-table-cell>
<s-table-cell as="th">Actions</s-table-cell>
</s-table-row>
</s-table-head>
<s-table-body>
{items.map(item => (
<s-table-row key={item.id}>
<s-table-cell>{item.name}</s-table-cell>
<s-table-cell>
<s-badge tone={item.isActive ? "success" : undefined}>
{item.isActive ? "Active" : "Inactive"}
</s-badge>
</s-table-cell>
<s-table-cell>{new Date(item.createdAt).toLocaleDateString()}</s-table-cell>
<s-table-cell>
<s-button-group>
<s-button variant="plain">Edit</s-button>
<s-button variant="plain" tone="critical">Delete</s-button>
</s-button-group>
</s-table-cell>
</s-table-row>
))}
</s-table-body>
</s-table>
</s-card>
</s-section>
</s-page>
); }
Pattern 2: Detail/Edit Page
export const loader = async ({ params, request }: LoaderFunctionArgs) => { const { session } = await authenticate.admin(request); const shop = await db.shop.findUnique({ where: { shopDomain: session.shop } });
const item = await db.item.findUnique({ where: { id: params.id, shopId: shop.id, }, });
if (!item) { throw new Response("Not Found", { status: 404 }); }
return json({ item }); };
export const action = async ({ params, request }: ActionFunctionArgs) => { const formData = await request.formData(); const name = formData.get("name"); const description = formData.get("description");
await db.item.update({ where: { id: params.id }, data: { name, description }, });
return redirect("/app/items"); };
export default function ItemDetailPage() { const { item } = useLoaderData<typeof loader>();
return ( <s-page heading={item.name} backUrl="/app/items"> <form method="post"> <s-card> <s-stack gap="400" direction="vertical"> <s-text-field label="Name" name="name" defaultValue={item.name} required /> <s-text-field label="Description" name="description" defaultValue={item.description} multiline={4} /> <s-button-group> <s-button type="submit" variant="primary">Save</s-button> <s-button url="/app/items">Cancel</s-button> </s-button-group> </s-stack> </s-card> </form> </s-page> ); }
Pattern 3: Modal Pattern
function ItemModal({ item, onClose }) { const submit = useSubmit();
function handleSubmit(event) { event.preventDefault(); const formData = new FormData(event.target); submit(formData, { method: "post" }); onClose(); }
return ( <s-modal open onClose={onClose} title="Edit Item"> <form onSubmit={handleSubmit}> <s-modal-section> <s-stack gap="400" direction="vertical"> <s-text-field label="Name" name="name" defaultValue={item?.name} /> <s-text-field label="Description" name="description" defaultValue={item?.description} multiline={3} /> </s-stack> </s-modal-section> <s-modal-footer> <s-button-group> <s-button onClick={onClose}>Cancel</s-button> <s-button type="submit" variant="primary">Save</s-button> </s-button-group> </s-modal-footer> </form> </s-modal> ); }
Pattern 4: Empty State
{items.length === 0 ? ( <s-card> <s-empty-state heading="No items yet" image="https://cdn.shopify.com/..." > <s-text variant="bodyMd"> Start by adding your first item </s-text> <s-button variant="primary" url="/app/items/new"> Add Item </s-button> </s-empty-state> </s-card> ) : ( // Item list )}
Pattern 5: Loading State
import { useNavigation } from "@remix-run/react";
export default function ItemsPage() { const navigation = useNavigation(); const isLoading = navigation.state === "loading";
return ( <s-page heading="Items"> {isLoading ? ( <s-card> <s-stack gap="400" direction="vertical"> <s-skeleton-display-text /> <s-skeleton-display-text /> <s-skeleton-display-text /> </s-stack> </s-card> ) : ( // Content )} </s-page> ); }
Pattern 6: Form with Validation
export const action = async ({ request }: ActionFunctionArgs) => { const formData = await request.formData(); const name = formData.get("name");
const errors = {}; if (!name) errors.name = "Name is required"; if (name.length < 3) errors.name = "Name must be at least 3 characters";
if (Object.keys(errors).length > 0) { return json({ errors }, { status: 400 }); }
await db.item.create({ data: { name } }); return redirect("/app/items"); };
export default function NewItemPage() { const actionData = useActionData<typeof action>();
return ( <form method="post"> <s-text-field label="Name" name="name" error={actionData?.errors?.name} required /> <s-button type="submit" variant="primary">Save</s-button> </form> ); }
Best Practices
-
Consistent Layouts - Use the same page structure across the app
-
Loading States - Always show skeleton loaders during data fetching
-
Empty States - Provide clear guidance when no data exists
-
Error Handling - Show user-friendly error messages
-
Form Validation - Validate on submit, show inline errors
-
Responsive Design - Test on mobile, tablet, and desktop
-
Accessibility - Use semantic HTML and ARIA attributes
-
SSR Compatibility - Avoid hydration mismatches
-
Performance - Lazy load components when appropriate
-
User Feedback - Show success/error toasts after actions
Remember: Consistent UI patterns create a professional, predictable user experience.