Product screen

ProReact Native · Expo

A shop's product detail: swipeable gallery with page pills, title, rating and a price that rolls with the size, color swatches or chips, size chips, a quantity stepper, specs, a reviews summary and a bottom bar with the rolling total. When Add to cart succeeds the product photo flies in an arc into the cart button and its count pops.

View as Markdown
Try it on your phone
Scan to open this demo in your phone's browser and use it with your fingers.

Installation

$ npx shadcn@latest add @goodui-native-pro/product-screen

Then run npx expo install --fix so react-native-reanimated, react-native-svg match your Expo SDK. Files land in components/goodui-native/product-screen.

Pro screens install from the license-gated @goodui-native-pro registry. Setup: Pro access.

  • Wrap the app in GestureHandlerRootView and ThemeProvider.

Usage

// app/product/[id].tsx
import { Stack, router, useLocalSearchParams } from "expo-router";
import { ProductScreen } from "@/components/goodui-native/product-screen/product-screen";
import { useCart } from "@/lib/cart";
import { useProduct } from "@/lib/shop";

export default function ProductRoute() {
  const { id } = useLocalSearchParams<{ id: string }>();
  const product = useProduct(id); // { title, price, currency: "EUR", images, colors, sizes, rating, reviewCount, ... }
  const cart = useCart();
  return (
    <>
      <Stack.Screen options={{ headerShown: false }} />
      <ProductScreen
        product={product}
        cartCount={cart.count}
        onAddToCart={(item) => cart.add(item)} // return the promise; the photo flies in when it resolves
        onCartPress={() => router.push("/cart")}
        onBack={() => router.back()}
        onShare={() => share(product)}
        favorite={product.saved}
        onFavoriteChange={(saved) => api.setSaved(id, saved)}
        onImagePress={(index) => router.push(`/product/${id}/photos?index=${index}`)}
        onSeeAllReviews={() => router.push(`/product/${id}/reviews`)}
      />
    </>
  );
}

When to use

Use it for
  • The product page of a shop, marketplace or drop app: one item with photos, variants and a cart.
  • Any catalog item people configure before buying (color, size, quantity).
Reach for something else
  • Browsing many products: build a grid or list and open this screen per item.
  • Services booked by time (a haircut, a class): use the booking screens.
  • Digital subscriptions: use paywall-screen.

Examples

On its own, with a sample product (photo placeholder, color chips)

<ProductScreen onBack={() => router.back()} />

Color swatches that switch the gallery photo

colors: [
  { value: "forest", label: "Forest", swatch: "#2F4A3A", imageIndex: 0 },
  { value: "sand", label: "Sand", swatch: "#C9B48A", imageIndex: 2 },
]

API reference

PropTypeDefault
productProductdefaultProduct
cartCount
Items in the cart, shown on the cart button (controlled). Without it the screen counts its own adds.
number—
onAddToCart
Add to cart. Return a promise: the button shows its states and the item flies into the cart when it resolves.
(item: AddToCartItem) => unknown() => new Promise((resolve) => setTimeout(resolve, 700))
onBack
Back button. Hidden when omitted.
() => void—
onCartPress
Cart button in the header.
() => void—
onShare
Share button in the header. Hidden when omitted.
() => void—
onFavoriteChange
Wishlist heart. Return a promise for an optimistic toggle with rollback.
(favorite: boolean) => unknown—
favorite
Wishlist state (controlled).
boolean—
onImagePress
Tap on the centered gallery image, for example to open an image viewer.
(index: number) => void—
onSeeAllReviews
"See all reviews". Hidden when omitted.
() => void—
locale
BCP 47 locale for prices and dates. Defaults to the device locale.
string—
labels
Overrides for the built-in strings; a GooduiLocaleProvider pack sets them app-wide.
Partial<ProductScreenLabels>—
style
Style for the outer container.
StyleProp<ViewStyle>—
testID
Identifier for end-to-end tests.
string—

Motion and haptics

  • Add to cart: the button gathers into a spinner and opens into 'Added'; then the current photo lifts from the button and flies in an arc (x eases out, y eases in, a little upward throw) into the cart button, shrinking and turning, and the cart badge pops with the new count.
  • Price and total roll when the size or quantity changes. Picking a color with an imageIndex glides the gallery to that photo.
  • The gallery follows the finger, neighbours shrink and the active pill stretches (Carousel).
  • Reduced motion: no flight (the count just updates), digits change in place, the carousel settles on a short timing, swatch rings and chip highlights jump.
  • Haptic: tap on Add to cart, success or error when its promise settles (Button)
  • Haptic: select when a swipe or a pill changes the photo (Carousel)
  • Haptic: select on a color or size change (ColorPicker, ChipGroup); warning at the stepper's limits
  • Haptic: pop on the wishlist heart (LikeButton)
  • The flight is one absolutely positioned view driven by a single shared value on the UI thread; it mounts only while flying.
  • Positions are measured with measureInWindow when the add resolves, so the flight lands on the cart wherever the header puts it.

Accessibility

  • The gallery is one adjustable element ('<title> photos', 'Photo 2 of 4, Forest'); swipe up or down, or use arrow keys on the web.
  • The title is a header; the wishlist heart, share, back and cart are labelled buttons ('Cart, 2 items').
  • Colors are a swatch group named 'Color' with each swatch's name, or a chip group; sizes are a chip group where sold out sizes are disabled and say 'sold out' in their label.
  • The quantity stepper is adjustable and named 'Quantity, <title>'; 'Only n left' is visible text.
  • Each rating bar is read as '5 stars, 70 percent'. A successful add is announced: 'Added 2 to your cart'.
  • The flying photo is hidden from screen readers.

Notes for AI

  • Pass product.images as require() sources or URLs; without images a placeholder shows and nothing flies (the count still updates).
  • Colors render as swatches only when every color has a swatch '#RRGGBB'; otherwise as chips. imageIndex ties a color to a gallery photo.
  • sizes[].priceDelta is added to price; soldOut sizes are disabled. stock caps the quantity at min(stock, 10) and shows 'Only n left' at 5 or fewer.
  • onAddToCart receives { productId, color, size, quantity, unitPrice } and should return a promise. Pass cartCount to show your real cart count; without it the screen counts its own adds.
  • Header buttons appear only for the callbacks you pass: onBack, onShare. Render it full-bleed with the navigator header hidden (headerShown: false).
Cart screen

A shopping cart: line items with a quantity stepper whose minus turns into remove, swipe left to remove or save for later, a promo code field, a free delivery meter and totals that roll with every change above a checkout bar. Removing an item offers Undo; an empty cart shows its own empty state.

Carousel

A snapping card carousel that follows the finger 1:1, shrinks and dims the neighbours, stretches its pagination pill with the drag, and turns that pill into a timer when it autoplays.

Color picker

Swatches with a selection ring that springs from swatch to swatch while the chosen swatch shrinks inside it, plus an optional pad: drag the thumb over saturation and brightness, slide the hue bar, and the before and after preview follows live.

Chip

Filter, choice and input chips: selecting one fills it with the accent while a check draws itself in, removing one shrinks it away, and a ChipGroup in single mode moves one elastic highlight from chip to chip.

Quantity stepper

A − value + stepper whose value rolls in the direction of change. Hold a button to repeat with acceleration; the ends push back with a rubber-band bounce and a warning haptic. In a cart the − turns into a trash button at the minimum.

Rating

Stars (or any symbol) you tap or drag across: the fill sweeps from the old rating to the new one and every symbol it crosses pops on its own beat; half steps optional, and a read-only variant shows exact fractions with a value and a count.

Like button

A like toggle with one moment of delight: the heart squashes and springs back filled, a ring expands and particles burst while the count rolls; it updates optimistically and rolls back if the request fails.