Skip to content

Components / Sections

Product Detail

The top of a product's page: the product drawn large on its plate (ProductArt), with its colourways under it as small swatches (radio buttons) that recolour the drawing in place; beside it the trail back to the shop, the name, the price, a line, how many of the batch are left, two actions, the specifications in the mono face and a few notes that open (care, delivery). On a wide screen the picture holds while the details scroll past it. Zero JavaScript.

Live demo

Glaze

Tall vase

Price: $86

A tall vase for branches and long stems, thrown in two parts and joined at the shoulder.

9 of 24 left in this batch

Size
32 cm tall, 14 cm across
Clay
Speckled stoneware
Care
Wash by hand
Delivery

Wrapped in paper and card, and sent the week after it comes out of the kiln.

Live, from the library itself, at full size: scroll, hover, navigate away and back. The demo is never gated.

Variants · each row writes the attribute the prop writes; the section's own CSS answers

density

Measured

JavaScript of its own

0 B

CSS 20.6 kB raw including the base tokens. Measured from a production build, and again in CI for every change that can move it.

Browser support

Baseline · newly available

Chrome 105 · Firefox 121 · Safari 15.4; needs has. Elsewhere: the first colourway shows and the swatches do not recolour it.

Install Pro

The demo above is never gated; the source is. With a key in components.json (see account), the registry serves it like any other item:

terminal
npx shadcn@latest add @moonarc-pro/product-detail
src/pages/index.astro
---
import ProductDetail from '@/components/moonarc/pro/ProductDetail.astro';
---
<ProductDetail
  name="Tall vase"
  art={{ name: 'vase', block: 1 }}
  ways={[
    { name: 'Sage', colour: 'var(--ma-block-2)', accent: 'var(--ma-block-3)' },
    { name: 'Ochre', colour: 'var(--ma-block-3)' },
  ]}
  price="$86"
  lede="A tall vase for branches and long stems."
  trail={[{ href: '/pieces/', label: 'Pieces' }]}
  batch={{ made: 24, left: 9 }}
  primary={{ href: '#reserve', label: 'Reserve one' }}
  specs={[{ label: 'Size', value: '32 cm tall' }, { label: 'Clay', value: 'Stoneware' }]}
  notes={[{ title: 'Delivery', body: 'Sent the week after it comes out of the kiln.' }]}
  transition="art-tall-vase"
/>

Without a key the registry answers 401 with an upgrade message. Everything this section composes is free: product-art.

Props

PropTypeDefaultDescription
namestringnoneThe product's name: the page's heading (level), and the picture's label.
art{ name: string; block?: 1 | 2 | 3 | 4 | 5 }noneThe ProductArt drawing and the colour block of its plate.
ways{ name: string; colour: string; accent?: string }[]noneOne to six colourways; the first is shown first. colour and accent are any CSS colour (var(--ma-block-2) follows the palette).
pricestringnoneThe price, in the mono face.
unitstringnoneAfter the price, smaller: per bag, each.
ledestringnoneA line under the price.
trail{ href: string; label: string }[]noneThe trail back: the shop, a kind. A nav named by trailLabel.
batch{ made: number; left: number; text?: string }noneHow many the batch makes and how many are left; text is the sentence with {left} and {made}.
primary{ href: string; label: string }noneThe main action, e.g. to the pre-order form.
secondary{ href: string; label: string }noneThe second action, outlined.
specs{ label: string; value: string }[]noneThe specifications, as a description list.
notes{ title: string; body: string }[]noneShort notes under the specifications, each a details element that opens on its title.
transitionstringnoneA view-transition-name for the picture, unique on the page: a card that names its picture the same way hands it over.
waysLabelstring'Colour'The colourways' legend.
trailLabelstring'Breadcrumb'Accessible name of the trail.
priceLabelstring'Price'The words a screen reader hears before the price.
level1 | 21The name's heading level: 1 as the product page's own heading, 2 under another h1.
density'compact' | 'cozy' | 'roomy''cozy'Block padding ×0.8 / ×1 / ×1.25 through --ma-density.

Theming and overrides

The section reads the theme contract (shadcn's variable names with the base tokens as fallback), so a pasted theme or one --ma-brand recolours it. Its class names are the override surface and stay stable; a named slot replaces the matching prop with your own markup.

Classes

.ma-art.ma-pdetail.ma-pdetail__actions.ma-pdetail__batch.ma-pdetail__btn.ma-pdetail__btn--primary.ma-pdetail__btn--secondary.ma-pdetail__count.ma-pdetail__dot.ma-pdetail__info.ma-pdetail__inner.ma-pdetail__lede.ma-pdetail__legend.ma-pdetail__media.ma-pdetail__meter.ma-pdetail__name.ma-pdetail__note.ma-pdetail__notes.ma-pdetail__plate.ma-pdetail__price.ma-pdetail__specs.ma-pdetail__swatches.ma-pdetail__trail.ma-pdetail__unit.ma-pdetail__way.ma-pdetail__way-name.ma-pdetail__ways

Slots

none

Reduced motion

A new colourway paints at once instead of easing in, and the batch bar stands at its value.

With ClientRouter

CSS only; nothing to rebind.

Why it is built this way

Replaces: Product pages (Shopify themes) · Variant pickers. See the migration table.

Product Art

Backgrounds

A product drawn flat, for a shop slot that has no photograph yet: 29 drawings in six sets (clothes, ceramics, coffee, skincare, furniture and objects, bakery).

every browserhover

Contact · sample page

Write to the team.

Sample channels on the left. The form on the right is real and sends nothing here: fill it in and watch the button.

Email (sample)
hello@studio.example
Phone (sample)
+1 555 0100
Office (sample)
1 Sample Street, Springfield

Demo: nothing is sent.

A contact section in two columns: the ways to reach you (email, phone, office) on one side, a form that really sends on the other.

Chrome 135 · not Firefox · Safari 27click

The shop

Pieces from the current firing

A whole collection on one page, filtered by kind: the products as cards (each drawn on its plate with ProductArt, a hang tag for the price, the name as the card's link and a spec line in the mono face) in two, three or four columns by the box's width, and above them a row of filters with their counts (core FilterGrid: the cards that stay slide into place, and the URL keeps the filter).

newly · Chrome 137 · Firefox 144 · Safari 18.4hoverclick

A row of products that runs off the page's edge: each drawn on its plate (ProductArt) with a hang tag for the price, the name as the card's link and a spec line in the mono face.

Chrome 116 · not Firefox · Safari 26hoverscroll

Also: view transitions · how costs are measured · browser support · accessibility policy · five-minute setup · MCP for agents · this page as Markdown