Fliin
On this page

Screens / commerce

Product page

Presents a product with its photo gallery, price, rating, color, size and quantity, a collapsible description and add to cart always in reach, with loading, sold out, error and offline states.

16 of 16 variants pass
Product page, sale state
Platform
Brightness
Text scale
Direction

Import

Product page is part of Fliin Pro. With a license key, sign in once with fliin login, then add it in a Flutter app where fliin init has run:

fliin login
fliin add product

Also installs: accordion, alert, app-bar, button, close-button, core, empty-state, field, icons, page, page-indicator, result-card, section-header, segmented, skeleton, stepper, tag-group, text, text-field

Examples

Sale

A reduced price: compareAtPrice shows the old one struck through after it, formatted by your app like the price.

Product page, sale state
import 'package:your_app/ui/fliin/screens/product/product_controller.dart';
import 'package:your_app/ui/fliin/screens/product/product_data.dart';
import 'package:your_app/ui/fliin/screens/product/product_screen.dart';
import 'package:flutter/widgets.dart';

class ProductSaleExample extends StatelessWidget {
  const ProductSaleExample({super.key});

  @override
  Widget build(BuildContext context) {
    const p = sampleProduct;
    final product = Product(
      id: p.id,
      title: p.title,
      brand: p.brand,
      price: '39,00 €',
      compareAtPrice: '49,00 €',
      rating: p.rating,
      reviews: p.reviews,
      photos: p.photos,
      description: p.description,
      colors: p.colors,
      maxQuantity: p.maxQuantity,
    );
    return ProductScreen.standalone(
      service: const SampleProductService(latency: Duration(milliseconds: 600)),
      productId: p.id,
      initial: ProductReady(product, color: 'sand'),
      favoriteIcon: const SampleFavoriteIcon(),
      favoriteSelectedIcon: const SampleFavoriteIcon(filled: true),
      onBack: () {},
      onOpenProduct: (_) {},
    );
  }
}

Ready

The product: gallery, price, color and quantity, and the action that adds it to the cart. standalone holds its own controller over the service you pass.

Product page, ready state
import 'package:your_app/ui/fliin/screens/product/product_controller.dart';
import 'package:your_app/ui/fliin/screens/product/product_data.dart';
import 'package:your_app/ui/fliin/screens/product/product_screen.dart';
import 'package:flutter/widgets.dart';

class ProductReadyExample extends StatelessWidget {
  const ProductReadyExample({super.key});

  @override
  Widget build(BuildContext context) {
    return ProductScreen.standalone(
      service: const SampleProductService(latency: Duration(milliseconds: 600)),
      productId: sampleProduct.id,
      initial: const ProductReady(sampleProduct, color: 'sand'),
      favoriteIcon: const SampleFavoriteIcon(),
      favoriteSelectedIcon: const SampleFavoriteIcon(filled: true),
      onBack: () {},
      onOpenProduct: (_) {},
    );
  }
}

Loading

While the product loads, placeholders hold the place of the gallery and the details.

Product page, loading state
import 'package:your_app/ui/fliin/screens/product/product_controller.dart';
import 'package:your_app/ui/fliin/screens/product/product_data.dart';
import 'package:your_app/ui/fliin/screens/product/product_screen.dart';
import 'package:flutter/widgets.dart';

class ProductLoadingExample extends StatelessWidget {
  const ProductLoadingExample({super.key});

  @override
  Widget build(BuildContext context) {
    return ProductScreen.standalone(
      service: const SampleProductService(latency: Duration(milliseconds: 600)),
      productId: sampleProduct.id,
      initial: const ProductLoading(),
      favoriteIcon: const SampleFavoriteIcon(),
      favoriteSelectedIcon: const SampleFavoriteIcon(filled: true),
      onBack: () {},
      onOpenProduct: (_) {},
    );
  }
}

Unavailable

A sold-out product: the add action is disabled and says why.

Product page, unavailable state
import 'package:your_app/ui/fliin/screens/product/product_controller.dart';
import 'package:your_app/ui/fliin/screens/product/product_data.dart';
import 'package:your_app/ui/fliin/screens/product/product_screen.dart';
import 'package:flutter/widgets.dart';

class ProductUnavailableExample extends StatelessWidget {
  const ProductUnavailableExample({super.key});

  @override
  Widget build(BuildContext context) {
    return ProductScreen.standalone(
      service: const SampleProductService(latency: Duration(milliseconds: 600)),
      productId: sampleProduct.id,
      initial: const ProductReady(sampleSoldOutProduct),
      favoriteIcon: const SampleFavoriteIcon(),
      favoriteSelectedIcon: const SampleFavoriteIcon(filled: true),
      onBack: () {},
      onOpenProduct: (_) {},
    );
  }
}

Error

When loading fails, the screen says so and offers to try again.

Product page, error state
import 'package:your_app/ui/fliin/screens/product/product_controller.dart';
import 'package:your_app/ui/fliin/screens/product/product_data.dart';
import 'package:your_app/ui/fliin/screens/product/product_screen.dart';
import 'package:flutter/widgets.dart';

class ProductErrorExample extends StatelessWidget {
  const ProductErrorExample({super.key});

  @override
  Widget build(BuildContext context) {
    return ProductScreen.standalone(
      service: const SampleProductService(latency: Duration(milliseconds: 600)),
      productId: sampleProduct.id,
      initial: const ProductFailed(),
      favoriteIcon: const SampleFavoriteIcon(),
      favoriteSelectedIcon: const SampleFavoriteIcon(filled: true),
      onBack: () {},
      onOpenProduct: (_) {},
    );
  }
}

Offline

Without a connection, the screen asks to reconnect rather than showing an error.

Product page, offline state
import 'package:your_app/ui/fliin/screens/product/product_controller.dart';
import 'package:your_app/ui/fliin/screens/product/product_data.dart';
import 'package:your_app/ui/fliin/screens/product/product_screen.dart';
import 'package:flutter/widgets.dart';

class ProductOfflineExample extends StatelessWidget {
  const ProductOfflineExample({super.key});

  @override
  Widget build(BuildContext context) {
    return ProductScreen.standalone(
      service: const SampleProductService(latency: Duration(milliseconds: 600)),
      productId: sampleProduct.id,
      initial: const ProductOffline(),
      favoriteIcon: const SampleFavoriteIcon(),
      favoriteSelectedIcon: const SampleFavoriteIcon(filled: true),
      onBack: () {},
      onOpenProduct: (_) {},
    );
  }
}

Routing

fliin add product prints the route for the router it finds in your pubspec, a file next to the page that wires its navigation. Each of them:

go_router

// lib/product_route.dart, next to product_page.dart.
import 'package:go_router/go_router.dart';

import 'product_page.dart';

/// Add productRoute to the routes of your GoRouter.
final productRoute = GoRoute(
  path: '/product/:id',
  builder: (context, state) => ProductPage(
    id: state.pathParameters['id']!,
    // Alone in the stack (deep link, go): your app's home.
    onBack: () => context.canPop() ? context.pop() : context.go('/'),
    // The cart: Fliin's checkout screen, or yours.
    onAdded: () => context.push('/checkout'),
    onOpenProduct: (product) => context.push('/product/${Uri.encodeComponent(product.id)}'),
  ),
);

navigator

// lib/product_route.dart, next to product_page.dart.
import 'package:flutter/material.dart';

import 'product_page.dart';

/// Opens “Product page”: `Navigator.of(context).push(productRoute(id, cartPage: …))`.
/// [cartPage] builds the cart: the page of Fliin's checkout screen with its callbacks, or yours (checkoutRoute cannot come from here, checkout leads to order).
Route<void> productRoute(String id, {required WidgetBuilder cartPage}) => MaterialPageRoute<void>(
  builder: (context) => ProductPage(
    id: id,
    onBack: () => Navigator.of(context).maybePop(),
    onAdded: () => Navigator.of(context).push(MaterialPageRoute<void>(builder: cartPage)),
    onOpenProduct: (product) => Navigator.of(context).push(productRoute(product.id, cartPage: cartPage)),
  ),
);

State

The controller is a ChangeNotifier from widgets.dart, with no package. fliin add product prints the page that holds it with the state management it finds, its imports written for your package; below they read your_app. Your backend implements the Service interface of the screen's _data.dart file:

none

// lib/product_page.dart
// Your backend: implement ProductService (product_data.dart) and pass it instead of the sample service.
import 'package:your_app/ui/fliin/screens/product/product_data.dart';
import 'package:your_app/ui/fliin/screens/product/product_screen.dart';
import 'package:flutter/widgets.dart';

class ProductPage extends StatelessWidget {
  const ProductPage({
    super.key,
    required this.id,
    required this.onBack,
    required this.onAdded,
    required this.onOpenProduct,
  });

  final String id;
  final VoidCallback onBack;
  final VoidCallback onAdded;
  final ValueChanged<ProductSummary> onOpenProduct;

  @override
  Widget build(BuildContext context) => ProductScreen.standalone(
    key: ValueKey(id),
    service: const SampleProductService(),
    productId: id,
    favoriteIcon: const SampleFavoriteIcon(),
    favoriteSelectedIcon: const SampleFavoriteIcon(filled: true),
    onBack: onBack,
    onAdded: onAdded,
    onOpenProduct: onOpenProduct,
  );
}

provider

// lib/product_page.dart
// Your backend: implement ProductService (product_data.dart) and pass it instead of the sample service.
import 'package:your_app/ui/fliin/screens/product/product_controller.dart';
import 'package:your_app/ui/fliin/screens/product/product_data.dart';
import 'package:your_app/ui/fliin/screens/product/product_screen.dart';
import 'package:flutter/widgets.dart';
import 'package:provider/provider.dart';

class ProductPage extends StatelessWidget {
  const ProductPage({
    super.key,
    required this.id,
    required this.onBack,
    required this.onAdded,
    required this.onOpenProduct,
  });

  final String id;
  final VoidCallback onBack;
  final VoidCallback onAdded;
  final ValueChanged<ProductSummary> onOpenProduct;

  @override
  Widget build(BuildContext context) => ChangeNotifierProvider(
    create: (_) => ProductController(service: const SampleProductService(), productId: id)..load(),
    child: Builder(
      builder: (context) => ProductScreen(
        controller: context.read<ProductController>(),
        favoriteIcon: const SampleFavoriteIcon(),
        favoriteSelectedIcon: const SampleFavoriteIcon(filled: true),
        onBack: onBack,
        onAdded: onAdded,
        onOpenProduct: onOpenProduct,
      ),
    ),
  );
}

riverpod

// lib/product_page.dart
// Your backend: implement ProductService (product_data.dart) and pass it instead of the sample service.
import 'package:your_app/ui/fliin/screens/product/product_controller.dart';
import 'package:your_app/ui/fliin/screens/product/product_data.dart';
import 'package:your_app/ui/fliin/screens/product/product_screen.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

final productControllerProvider = Provider.autoDispose.family<ProductController, String>((ref, id) {
  final controller = ProductController(service: const SampleProductService(), productId: id)..load();
  ref.onDispose(controller.dispose);
  return controller;
});

class ProductPage extends ConsumerWidget {
  const ProductPage({
    super.key,
    required this.id,
    required this.onBack,
    required this.onAdded,
    required this.onOpenProduct,
  });

  final String id;
  final VoidCallback onBack;
  final VoidCallback onAdded;
  final ValueChanged<ProductSummary> onOpenProduct;

  @override
  Widget build(BuildContext context, WidgetRef ref) => ProductScreen(
    controller: ref.watch(productControllerProvider(id)),
    favoriteIcon: const SampleFavoriteIcon(),
    favoriteSelectedIcon: const SampleFavoriteIcon(filled: true),
    onBack: onBack,
    onAdded: onAdded,
    onOpenProduct: onOpenProduct,
  );
}

bloc

// lib/product_page.dart
// Your backend: implement ProductService (product_data.dart) and pass it instead of the sample service.
import 'package:your_app/ui/fliin/screens/product/product_controller.dart';
import 'package:your_app/ui/fliin/screens/product/product_data.dart';
import 'package:your_app/ui/fliin/screens/product/product_screen.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_bloc/flutter_bloc.dart';

/// The screen's controller as a bloc, for your BlocListener and BlocBuilder:
/// one emission per controller notification. `version` tells them apart
/// when `state` does not change (mode, filters, text…, read them on
/// `controller`). The screen listens to the controller itself.
class ProductCubit extends Cubit<({ProductState state, int version})> {
  ProductCubit(this.controller) : super((state: controller.state, version: 0)) {
    controller.addListener(_changed);
  }

  final ProductController controller;

  void _changed() => emit((state: controller.state, version: state.version + 1));

  @override
  Future<void> close() {
    controller
      ..removeListener(_changed)
      ..dispose();
    return super.close();
  }
}

class ProductPage extends StatelessWidget {
  const ProductPage({
    super.key,
    required this.id,
    required this.onBack,
    required this.onAdded,
    required this.onOpenProduct,
  });

  final String id;
  final VoidCallback onBack;
  final VoidCallback onAdded;
  final ValueChanged<ProductSummary> onOpenProduct;

  @override
  Widget build(BuildContext context) => BlocProvider(
    create: (_) => ProductCubit(ProductController(service: const SampleProductService(), productId: id)..load()),
    child: Builder(
      builder: (context) => ProductScreen(
        controller: context.read<ProductCubit>().controller,
        favoriteIcon: const SampleFavoriteIcon(),
        favoriteSelectedIcon: const SampleFavoriteIcon(filled: true),
        onBack: onBack,
        onAdded: onAdded,
        onOpenProduct: onOpenProduct,
      ),
    ),
  );
}

API

ProductScreen: class extends StatefulWidget
ProductScreen.new: const new({super.key, required ProductController controller, required Widget favoriteIcon, required Widget favoriteSelectedIcon, VoidCallback? onBack, VoidCallback? onAdded, ValueChanged<ProductSummary>? onOpenProduct})
ProductScreen.standalone: const new({super.key, required ProductService service, required String productId, required Widget favoriteIcon, required Widget favoriteSelectedIcon, ProductState? initial, VoidCallback? onBack, VoidCallback? onAdded, ValueChanged<ProductSummary>? onOpenProduct})
ProductScreen.controller: get ProductController?
ProductScreen.service: get ProductService?
ProductScreen.productId: get String?
ProductScreen.initial: get ProductState?
ProductScreen.favoriteIcon: get Widget
ProductScreen.favoriteSelectedIcon: get Widget
ProductScreen.onBack: get VoidCallback?
ProductScreen.onAdded: get VoidCallback?
ProductScreen.onOpenProduct: get ValueChanged<ProductSummary>?
ProductScreen.createState: State<ProductScreen>()
ProductState: sealed class permits {ProductFailed, ProductLoading, ProductOffline, ProductReady}
ProductState.new: const new()
ProductLoading: final class extends ProductState
ProductLoading.new: const new()
ProductFailed: final class extends ProductState
ProductFailed.new: const new()
ProductOffline: final class extends ProductState
ProductOffline.new: const new()
ProductNotice: enum {added, addFailed, addOffline, favoriteFailed, chooseSize}
ProductNotice.added: static final ProductNotice
ProductNotice.addFailed: static final ProductNotice
ProductNotice.addOffline: static final ProductNotice
ProductNotice.favoriteFailed: static final ProductNotice
ProductNotice.chooseSize: static final ProductNotice
ProductReady: final class extends ProductState
ProductReady.new: const new(Product product, {String? size, String? color, int quantity, bool adding, bool sizeMissing, ProductNotice? notice})
ProductReady.product: get Product
ProductReady.size: get String?
ProductReady.color: get String?
ProductReady.quantity: get int
ProductReady.adding: get bool
ProductReady.sizeMissing: get bool
ProductReady.notice: get ProductNotice?
ProductController: class extends ChangeNotifier
ProductController.new: new({required ProductService service, required String productId, ProductState initial})
ProductController.service: get ProductService
ProductController.productId: get String
ProductController.state: get ProductState
ProductController.load: Future<bool>()
ProductController.selectSize: bool(String id)
ProductController.selectColor: bool(String id)
ProductController.setQuantity: bool(int quantity)
ProductController.addToCart: Future<bool>()
ProductController.toggleFavorite: bool()
ProductController.dispose: void()
ProductPhoto: final class
ProductPhoto.new: const new(ImageProvider image, String description)
ProductPhoto.image: get ImageProvider
ProductPhoto.description: get String
ProductOption: final class
ProductOption.new: const new(String id, String label, {bool available})
ProductOption.id: get String
ProductOption.label: get String
ProductOption.available: get bool
ProductSummary: final class
ProductSummary.new: const new({required String id, required String title, required String price, ImageProvider? image})
ProductSummary.id: get String
ProductSummary.title: get String
ProductSummary.price: get String
ProductSummary.image: get ImageProvider?
Product: final class
Product.new: const new({required String id, required String title, required String price, required List<ProductPhoto> photos, required String description, String? brand, double? rating, int? reviews, String? details, String? delivery, List<ProductOption> sizes, List<ProductOption> colors, bool inStock, int maxQuantity, bool favorite, List<ProductSummary> similar, String? compareAtPrice})
Product.id: get String
Product.title: get String
Product.price: get String
Product.compareAtPrice: get String?
Product.photos: get List<ProductPhoto>
Product.description: get String
Product.brand: get String?
Product.rating: get double?
Product.reviews: get int?
Product.details: get String?
Product.delivery: get String?
Product.sizes: get List<ProductOption>
Product.colors: get List<ProductOption>
Product.inStock: get bool
Product.maxQuantity: get int
Product.favorite: get bool
Product.similar: get List<ProductSummary>
Product.withFavorite: Product(bool favorite)
CartLine: final class
CartLine.new: const new({required String productId, required int quantity, String? size, String? color})
CartLine.productId: get String
CartLine.quantity: get int
CartLine.size: get String?
CartLine.color: get String?
ProductNoNetwork: final class implements Exception
ProductNoNetwork.new: const new()
ProductService: abstract interface class
ProductService.new: new()
ProductService.load: abstract Future<Product>(String id)
ProductService.addToCart: abstract Future<void>(CartLine line)
ProductService.setFavorite: abstract Future<void>(String id, bool favorite)
sampleProduct: const dynamic
sampleSoldOutProduct: const dynamic
SampleProductService: final class implements ProductService
SampleProductService.new: const new({Product product, Duration latency})
SampleProductService.product: get Product
SampleProductService.latency: get Duration
SampleProductService.load: Future<Product>(String id)
SampleProductService.addToCart: Future<void>(CartLine line)
SampleProductService.setFavorite: Future<void>(String id, bool favorite)
SampleFavoriteIcon: class extends StatelessWidget
SampleFavoriteIcon.new: const new({super.key, bool filled})
SampleFavoriteIcon.filled: get bool
SampleFavoriteIcon.build: Widget(BuildContext context)
productL10n: const Map<String, Map<String, String>>
ProductStrings: final class
ProductStrings.new: const new(String language)
ProductStrings.of: factory new(BuildContext context)
ProductStrings.language: get String
ProductStrings.title: get String
ProductStrings.back: get String
ProductStrings.loading: get String
ProductStrings.photos: get String
ProductStrings.photoPage: String(int page, int count)
ProductStrings.photoUnavailable: get String
ProductStrings.openPhoto: get String
ProductStrings.closePhotos: get String
ProductStrings.rating: String(double rating, int count)
ProductStrings.ratingShown: String(double rating, int count)
ProductStrings.color: get String
ProductStrings.colorChosen: String(String color)
ProductStrings.size: get String
ProductStrings.chooseSize: get String
ProductStrings.quantity: get String
ProductStrings.decreaseQuantity: get String
ProductStrings.increaseQuantity: get String
ProductStrings.description: get String
ProductStrings.details: get String
ProductStrings.delivery: get String
ProductStrings.similar: get String
ProductStrings.addToCart: get String
ProductStrings.soldOut: get String
ProductStrings.soldOutMessage: get String
ProductStrings.favorite: get String
ProductStrings.favoriteAdded: get String
ProductStrings.favoriteRemoved: get String
ProductStrings.added: get String
ProductStrings.addFailed: get String
ProductStrings.addOffline: get String
ProductStrings.favoriteFailed: get String
ProductStrings.errorTitle: get String
ProductStrings.errorMessage: get String
ProductStrings.offlineTitle: get String
ProductStrings.offlineMessage: get String
ProductStrings.retry: get String
ProductStrings.wasPrice: String(String price)

Strings

Every label in English, French and Arabic. The screen follows the closest Localizations; the same table is in the manifest, ready for your ARB files.

Keyenfrar
productTitleProductProduitالمنتج
productBackBackRetourرجوع
productLoadingLoading the productChargement du produitجارٍ تحميل المنتج
productPhotosPhotosPhotosالصور
productPhotoPagePhoto {page} of {count}Photo {page} sur {count}الصورة {page} من {count}
productPhotoUnavailablePhoto unavailablePhoto indisponibleالصورة غير متاحة
productOpenPhotoOpen full screenOuvrir en plein écranفتح بملء الشاشة
productClosePhotosCloseFermerإغلاق
productRating{rating} out of 5 · {count} reviews{rating} sur 5 · {count} avis{rating} من 5 · التقييمات: {count}
productRatingShown{rating} · {count} reviews{rating} · {count} avis{rating} · التقييمات: {count}
productDecimal.,٫
productColorColorCouleurاللون
productColorChosenColor: {color}Couleur : {color}اللون: {color}
productSizeSizeTailleالمقاس
productChooseSizeChoose a size.Choisissez une taille.اختر مقاسًا.
productQuantityQuantityQuantitéالكمية
productDecreaseQuantityDecrease quantityDiminuer la quantitéإنقاص الكمية
productIncreaseQuantityIncrease quantityAugmenter la quantitéزيادة الكمية
productDescriptionDescriptionDescriptionالوصف
productDetailsMaterials and careComposition et entretienالخامات والعناية
productDeliveryDelivery and returnsLivraison et retoursالتوصيل والإرجاع
productSimilarYou may also likeVous aimerez aussiقد يعجبك أيضًا
productAddToCartAdd to cartAjouter au panierأضف إلى السلة
productSoldOutSold outÉpuiséنفدت الكمية
productSoldOutMessageThis product is no longer available.Ce produit n’est plus disponible.هذا المنتج لم يعد متاحًا.
productFavoriteFavoriteFavoriالمفضلة
productFavoriteAddedAdded to favoritesAjouté aux favorisأضيف إلى المفضلة
productFavoriteRemovedRemoved from favoritesRetiré des favorisأزيل من المفضلة
productAddedAdded to your cart.Ajouté au panier.أضيف إلى سلتك.
productAddFailedCouldn't add to your cart. Try again.Ajout au panier impossible. Réessayez.تعذّرت الإضافة إلى السلة. أعد المحاولة.
productAddOfflineYou are offline. Nothing was added.Vous êtes hors ligne. Rien n’a été ajouté.أنت غير متصل. لم تتم إضافة شيء.
productFavoriteFailedCouldn't update your favorites.Impossible de mettre à jour vos favoris.تعذّر تحديث المفضلة.
productErrorTitleThis product could not loadImpossible de charger ce produitتعذّر تحميل هذا المنتج
productErrorMessageSomething went wrong on our side.Un problème est survenu de notre côté.حدثت مشكلة من جهتنا.
productOfflineTitleYou are offlineVous êtes hors ligneأنت غير متصل
productOfflineMessageCheck your connection, then try again.Vérifiez votre connexion, puis réessayez.تحقق من اتصالك ثم أعد المحاولة.
productRetryTry againRéessayerأعد المحاولة
productWasPriceWas {price}Au lieu de {price}بدلًا من {price}

Reference renders

The 16 images published with version 0.2.5, rendered by the shared harness on macOS with Flutter 3.47.7. Each shows every state side by side, at the size of an iPhone (390 × 844) or of an Android phone (412 × 915). fliin verify product compares your copy to them.

iOS · Light
Product page, iOS, Light, text 1.0×, Left to right
Text 1.0× · Left to right
Product page, iOS, Light, text 1.0×, Right to left
Text 1.0× · Right to left
Product page, iOS, Light, text 2.0×, Left to right
Text 2.0× · Left to right
Product page, iOS, Light, text 2.0×, Right to left
Text 2.0× · Right to left
iOS · Dark
Product page, iOS, Dark, text 1.0×, Left to right
Text 1.0× · Left to right
Product page, iOS, Dark, text 1.0×, Right to left
Text 1.0× · Right to left
Product page, iOS, Dark, text 2.0×, Left to right
Text 2.0× · Left to right
Product page, iOS, Dark, text 2.0×, Right to left
Text 2.0× · Right to left
Android · Light
Product page, Android, Light, text 1.0×, Left to right
Text 1.0× · Left to right
Product page, Android, Light, text 1.0×, Right to left
Text 1.0× · Right to left
Product page, Android, Light, text 2.0×, Left to right
Text 2.0× · Left to right
Product page, Android, Light, text 2.0×, Right to left
Text 2.0× · Right to left
Android · Dark
Product page, Android, Dark, text 1.0×, Left to right
Text 1.0× · Left to right
Product page, Android, Dark, text 1.0×, Right to left
Text 1.0× · Right to left
Product page, Android, Dark, text 2.0×, Left to right
Text 2.0× · Left to right
Product page, Android, Dark, text 2.0×, Right to left
Text 2.0× · Right to left

Use it to

  • show the details of an item with its photos and price
  • choose the size and color of a garment before buying it
  • add an item to the bag or cart from its page
  • open the photos of an item and enlarge them
  • keep an item as a favorite for later
Not for
  • a product in a list of results: use a result card (result-card)
  • a sequence of photos alone: use a page indicator (page-indicator)
States
  • Ready
  • Loading
  • Unavailable
  • Error
  • Offline

Arrows to move, Enter to open, Escape to close.