Product screen
ProReact Native · ExpoA 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.
Installation
$ npx shadcn@latest add @goodui-native-pro/product-screenThen 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
- 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).
- 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
| Prop | Type | Default |
|---|---|---|
| product | Product | defaultProduct |
| 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).
Related
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.
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.
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.
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.
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.
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.
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.