sfnext-create-component

star 45

Author a new Storefront Next design-system component the right way — pick the correct layer (primitive vs composite), reuse-or-extend before creating, define CVA variants bound to semantic tokens, add data-slot attributes, satisfy the accessibility checklist, and ship a Storybook story. Use when asked to create, add, or build a new UI component, button/badge/tile/card variant, or design-system primitive. This is the design-system authoring discipline; for createPage/Suspense/shadcn page-rendering patterns see sfnext-components.

SalesforceCommerceCloud By SalesforceCommerceCloud schedule Updated 6/11/2026

name: sfnext-create-component description: Author a new Storefront Next design-system component the right way — pick the correct layer (primitive vs composite), reuse-or-extend before creating, define CVA variants bound to semantic tokens, add data-slot attributes, satisfy the accessibility checklist, and ship a Storybook story. Use when asked to create, add, or build a new UI component, button/badge/tile/card variant, or design-system primitive. This is the design-system authoring discipline; for createPage/Suspense/shadcn page-rendering patterns see sfnext-components.

Create a Component Skill

This skill is the authoring discipline for adding a new component to a Storefront Next storefront: choosing the right layer, reusing before creating, and meeting the token / a11y / Storybook requirements that keep the design system consistent.

It complements sfnext-components. That skill covers engineering patterns — createPage, Suspense/Await streaming, shadcn/ui setup, the cn() utility. This skill covers design-system authoring — the layer model, the extend-before-create gate, CVA variants bound to semantic tokens, data-slot, accessibility, and story coverage. Use both together when building UI.

Before you start — scan first (extend before create)

Do not create a new component file until you have run this scan:

  1. Search src/components/ for components with a similar name or behaviour.
  2. Search src/components/ui/ for Shadcn primitives — run npx shadcn@latest add <name> to check whether it exists before hand-rolling one.
  3. Apply the REUSE / EXTEND / CREATE decision:
    • REUSE — an existing component already does this → use it as-is.
    • EXTEND — an existing component is close → add a new CVA variant (cheaper than a new file).
    • CREATE — nothing exists → proceed with this skill.

See Component Authoring Reference for the full layer model and the extend-before-create rule.

Step 1 — Decide the layer

Question Layer Location
Pure UI element, no data or business logic? Primitive src/components/ui/ (check shadcn first)
Assembles primitives or takes data props? Composite src/components/

For primitives, always check shadcn before hand-rolling:

# from packages/template-retail-rsc-app
npx shadcn@latest add <component-name>

Shadcn components land in src/components/ui/ already wired to the token system. If it exists in shadcn, use it — do not hand-roll a replacement.

Step 2 — Create the file with the copyright header

src/components/<feature>/<component-name>.tsx    ← preferred (feature-grouped)
src/components/<component-name>.tsx              ← acceptable for standalone components

Start every .ts/.tsx file with the Apache 2.0 copyright header (ESLint enforces it), then imports:

import * as React from 'react'
import { cva, type VariantProps } from 'class-variance-authority'
import { cn } from '@/lib/utils'
// Import primitives from src/components/ui/ — not from @radix-ui directly
import { Button } from '@/components/ui/button'

Step 3 — Type the props explicitly

interface ProductTileProps {
  productId: string
  name: string
  price: number
  imageUrl: string
  currency?: string
  className?: string
}

No any. Extend React.ComponentProps<'element'> for native passthrough. Use discriminated unions for mutually exclusive prop shapes.

Step 4 — Define CVA variants (if the component has visual states)

const productTileVariants = cva(
  'relative flex flex-col overflow-hidden rounded-md bg-card text-card-foreground',
  {
    variants: {
      density: {
        comfortable: 'gap-4 p-4',
        compact: 'gap-2 p-2',
      },
    },
    defaultVariants: { density: 'comfortable' },
  }
)

Token rules: every class that affects color MUST use a semantic token utility (bg-primary, text-foreground, border-border, …). No raw hex, no bg-black, no text-gray-500. See Token System Reference.

Step 5 — Write the component

export function ProductTile({
  productId,
  name,
  price,
  imageUrl,
  currency = 'USD',
  className,
  density,
  ...props
}: ProductTileProps & VariantProps<typeof productTileVariants>) {
  return (
    <article
      data-slot="product-tile"
      data-product-id={productId}
      className={cn(productTileVariants({ density }), className)}
      {...props}
    >
      <div data-slot="product-tile-image">{/* image */}</div>
      <div data-slot="product-tile-body" className="flex flex-col gap-1">
        <span data-slot="product-tile-name" className="text-sm font-medium text-foreground">
          {name}
        </span>
        <span data-slot="product-tile-price" className="text-sm text-muted-foreground">
          {new Intl.NumberFormat('en-US', { style: 'currency', currency }).format(price)}
        </span>
      </div>
    </article>
  )
}

Required on every component:

  • data-slot on the root and each named region.
  • className accepted and merged via cn() last so callers can override.
  • Native props spread via {...props}.

Step 6 — Create the Storybook story

Every new component in src/components/ needs a .stories.tsx — one story per CVA variant plus a Snapshot story for CI diffing.

import type { Meta, StoryObj } from '@storybook/react'
import { ProductTile } from './product-tile'

const meta: Meta<typeof ProductTile> = {
  title: 'Components/Product/ProductTile',
  component: ProductTile,
}
export default meta

type Story = StoryObj<typeof ProductTile>

const defaultProps = { productId: 'P001', name: 'Classic Shirt', price: 89.99, imageUrl: '/placeholder.jpg' }

export const Default: Story = { args: defaultProps }
export const Compact: Story = { args: { ...defaultProps, density: 'compact' } }

export const Snapshot: Story = {
  name: 'Snapshot',
  render: () => (
    <div className="grid grid-cols-2 gap-4">
      <ProductTile {...defaultProps} density="comfortable" />
      <ProductTile {...defaultProps} density="compact" />
    </div>
  ),
}

Step 7 — Accessibility check

  • Interactive elements are <button> or <a>, never <div onClick>.
  • All form inputs have <Label htmlFor>.
  • Focus ring: focus-visible:ring-ring focus-visible:ring-[3px].
  • No color-only information (pair with icon or text).
  • aria-label on icon-only buttons.

Verification

Before marking the task complete, run from the monorepo root:

pnpm typecheck
pnpm lint
pnpm test-storybook:snapshot:agent

All three must pass with zero errors and zero warnings. Lint runs with --max-warnings 0.

Related Skills

  • storefront-next:sfnext-components - createPage HOC, Suspense/Await, shadcn/ui, Tailwind page-rendering patterns
  • storefront-next:sfnext-testing - Vitest unit tests and Storybook interaction/a11y tests
  • storefront-next:sfnext-create-vertical - Brand theming via the token layer
  • storefront-next:sfnext-extensions - Injecting a component into a page via UITarget

Reference Documentation

Install via CLI
npx skills add https://github.com/SalesforceCommerceCloud/b2c-developer-tooling --skill sfnext-create-component
Repository Details
star Stars 45
call_split Forks 11
navigation Branch main
article Path SKILL.md
More from Creator
SalesforceCommerceCloud
SalesforceCommerceCloud Explore all skills →