---
title: "Product Gallery — React"
canonical: https://sky-ui.cf.sky.com/components/product-gallery/react
apiPackages: [{"name":"@sky-uk/ui-core","representedVersion":"13.2.0"}]
---

# Product Gallery — React

The Product Gallery component is used to visually showcase the features of a product.

```js
import { ProductGallery } from '@sky-uk/ui-core';
```

---

The Product Gallery component is used to visually showcase the features of a product

```tsx
<Flex $justifyContent="center">
  <ProductGallery
    $size="small"
    $thumbnailRailPlacement="bottom"
    $aspectRatio="1 / 1"
    $navigation
    $expand
  >
    <ProductGallery.Item>
      <Image alt="" role="presentation" src="https://placehold.co/100x100"/>
    </ProductGallery.Item>
    <ProductGallery.Item>
      <Image alt="" role="presentation" src="https://placehold.co/100x100"/>
    </ProductGallery.Item>
    <ProductGallery.Item>
      <Image alt="" role="presentation" src="https://placehold.co/100x100"/>
    </ProductGallery.Item>
    <ProductGallery.Item>
      <Image alt="" role="presentation" src="https://placehold.co/100x100"/>
    </ProductGallery.Item>
    <ProductGallery.Item>
      <Image alt="" role="presentation" src="https://placehold.co/100x100"/>
    </ProductGallery.Item>
  </ProductGallery>
</Flex>
```

---

## Props

### ProductGallery

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| $aspectRatio | string | 1 / 1 | The [aspect ratio](/core/system/aspect-ratio) prop can be changed to set the aspect ratio of the main image container. |
| $navigation | boolean | true | Icon Buttons to allow navigation to the previous or next gallery item. It will be applied to all of the items in the gallery. This is a [responsive](/core/hooks/use-responsive-props/) prop. |
| $scrollableRegionTabIndex | boolean | false | If set, the main item container will be focusable. This is used for accessibility purposes.<br><br>Note: this prop should only be used if the the main item container has non-interactive content and `$expand` is set to false. |
| $size | string | small | Sets the size variant of the Product Gallery. This prop is [responsive](/core/hooks/use-responsive-props/) and can be used to set the size of the Product Gallery at different breakpoints. |
| $thumbnailRailPlacement | string | bottom | The placement of the thumbnail rail. This prop is [responsive](/core/hooks/use-responsive-props/) and shows a preview of the other images within the gallery |
| $expand | boolean | false | Icon Button to maximise the main image, opening it as an overlay. It will be applied to all of the items in the gallery. |
| $thumbnailOutline | boolean | false | An outline to thumbnails that provides contrast against the background the gallery is used on. |

### ProductGallery.Item

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| $expand | boolean | false | Icon Button to maximise the main image, opening it as an overlay. It will be applied only to item that is specified in. |
| thumbnailSrc | string | - | Sets the image source for the thumbnail of the specified item. This prop takes precedence over the child element passed into the item. |
| thumbnailAlt | string | - | Sets the image alt text for the thumbnail image set by the thumbnailSrc. |

### $size

The `$size` prop can be set to `small` (default) or `large` depending on the viewport size.

```full-screen-react-live
<Flex $justifyContent="center">
  <ProductGallery
    $size={{xs: "small", md: "large"}}
    $thumbnailRailPlacement="bottom"
    $aspectRatio="16 / 9"
    $navigation
    $expand
  >
    {Array.from({ length: 7 }, (_, index) => (
      <ProductGallery.Item key={index}>
      <Image alt="" role="presentation" src="https://placehold.co/160x90"/>
      </ProductGallery.Item>
    ))}
  </ProductGallery>
</Flex>
```

### $thumbnailRailPlacement

The `$thumbnailRailPlacement` prop can be set to `bottom` (default) or `side`.

```full-screen-react-live
<Box $display="flex" $padding={4} $justifyContent="center">
  <ProductGallery
    $size={{sm: "small", lg: "large"}}
    $thumbnailRailPlacement="side"
    $aspectRatio="16 / 9"
    $navigation
    $expand
  >
    {Array.from({ length: 7 }, (_, index) => (
      <ProductGallery.Item key={index}>
        <Image alt="" role="presentation" src="https://placehold.co/160x90"/>
      </ProductGallery.Item>
    ))}
  </ProductGallery>
</Box>
```

### $expand

`$expand` is a boolean property that when passed in to `ProductGallery` renders expand controls on all items within the gallery. This property can also be set on individual `ProductGallery.Item` components if expand controls are only required for specific items. `$expand` set on the `ProductGallery` will take precedence.

**Note:** The `$expand` prop only adds the expand icon button to the component. The expand functionality itself must be implemented using the `onExpandControl` callback as documented below.

```tsx
<Flex $justifyContent="center">
  <ProductGallery
    $size="small"
    $thumbnailRailPlacement="bottom"
    $aspectRatio="16 / 9"
    $navigation
    $expand
  >
    <ProductGallery.Item>
      <Image alt="" role="presentation" src="https://placehold.co/1600x900"/>
    </ProductGallery.Item>
    <ProductGallery.Item>
      <Image alt="" role="presentation" src="https://placehold.co/1600x900"/>
    </ProductGallery.Item>
    <ProductGallery.Item>
      <Image alt="" role="presentation" src="https://placehold.co/1600x900"/>
    </ProductGallery.Item>
    <ProductGallery.Item>
      <Image alt="" role="presentation" src="https://placehold.co/1600x900"/>
    </ProductGallery.Item>
    <ProductGallery.Item>
      <Image alt="" role="presentation" src="https://placehold.co/1600x900"/>
    </ProductGallery.Item>
  </ProductGallery>
</Flex>
```

---

## Variant Usage

To adhere to the variant governance set in [usage](/components/product-gallery/usage/#variants-usage), avoid using `$size="large"` and `$thumbnailRailPlacement="side"` in the
`ProductGallery` component on small screens (e.g., mobile). This combination may negatively impact usability and layout. Instead, choose a configuration better suited for smaller viewports.

```full-screen-react-live
<Box $display="flex" $padding={{ xs: 4, md: 5 }} $justifyContent="center">
  <ProductGallery
    $size={{ xs: 'small', md: 'large' }}
    $thumbnailRailPlacement={{ xs: 'bottom', sm: 'side', md: 'bottom', lg: 'side' }}
    $aspectRatio="4 / 3"
    $navigation
    $expand
  >
    {Array.from({ length: 7 }, (_, index) => (
      <ProductGallery.Item key={index}>
        <Image alt="" role="presentation" src="https://placehold.co/400x300" />
      </ProductGallery.Item>
    ))}
  </ProductGallery>
</Box>
```

---

## Callbacks

### onMount

The `onMount` callback will return an `array` containing `ref`s to all the items. This can be used to manage the state of the slides.

```tsx
() => {

  function Example(){
    const [allItems, setAllItems] = React.useState([]);

  const handleMount = e => {
    setAllItems(e);
  };

  return (
    <Flex $flexDirection="column" $gap={5}>
      <Flex $flexDirection="row" $gap={4}>
        <Text>All Items:</Text>
        <ul>
          {allItems.map((item, idx) => (
            <Text as="li" key={idx}>
              <code>{item.ref && item.ref.current && item.ref.current.className}</code>
            </Text>
          ))}
        </ul>
      </Flex>
      <Flex $justifyContent="center">
        <ProductGallery $size="small" $thumbnailRailPlacement="bottom" $aspectRatio="16 / 9" $navigation onMount={handleMount} $scrollableRegionTabIndex={true}>
          {Array.from({ length: 7 }, (_, index) => (
            <ProductGallery.Item key={index}>
              <Image alt="" role="presentation" src="https://placehold.co/160x90" />
            </ProductGallery.Item>
          ))}
        </ProductGallery>
      </Flex>
    </Flex>
  );
}

  return <Example />

}
```

### onItemChange

The `onItemChange` callback returns an object representing the current item once the transition is complete. The `current` key is the 0 indexed current item as a `number`. The `ref` key is a `ref` to the current item.

```tsx
() => {

 function Example(){
    const [itemState, setItemState] = React.useState({
      current: null
    });

    const handleItemChange = e => {
      setItemState(e);
    };

    return (
      <Flex $flexDirection="column" $gap={5}>
        <Text>Current item: {itemState.current}</Text>
        <Flex $justifyContent="center">
          <ProductGallery
            $size="small"
            $thumbnailRailPlacement="bottom"
            $aspectRatio="16 / 9"
            $navigation
            onItemChange={handleItemChange}
            $scrollableRegionTabIndex={true}
          >
            {Array.from({ length: 7 }, (_, index) => (
              <ProductGallery.Item key={index}>
                <Image alt="" role="presentation" src="https://placehold.co/160x90" />
              </ProductGallery.Item>
            ))}
          </ProductGallery>
        </Flex>
      </Flex>
    );
  }

  return <Example />

}
```

### onExpandControl

The `onExpandControl` callback will return the state of the expand control as an `object` when any of the expand control state changes. Management of the expand property is the concern of the consumer.

```tsx
() => {

function Example() {
  const [expandState, setExpandState] = React.useState(false);
  const [itemState, setItemState] = React.useState({ ref: null });
  const [modalContent, setModalContent] = React.useState(null);
  const [expandButtonRef, setExpandButtonRef] = React.useState(null);

  React.useEffect(() => {
    if (!itemState.ref || !itemState.ref.current) return;
    const itemRefCurrent = itemState.ref.current;
    const firstChild = itemRefCurrent.firstChild;

    if (firstChild instanceof HTMLImageElement) {
      setModalContent(<Image src={firstChild.src} />);
    } else if (firstChild instanceof HTMLVideoElement) {
      setModalContent(<Video controls src={firstChild.src} />);
    }
  }, [itemState]);

  const handleExpandChange = (btnRef, data) => {
    setExpandState(true);
    setExpandButtonRef(btnRef);
    setItemState(data);
  };

  const handleItemChange = () => {
    setExpandState(false);
  };

  const handleModalClose = e => {
    e.stopPropagation();
    setExpandState(false);
    if (expandButtonRef && expandButtonRef.current) {
      expandButtonRef.current.focus();
    }
  };

  return (
    <>
      {expandState && (
        <Modal onClose={handleModalClose}>
          <Modal.CloseButton $inline={false} />
          <Modal.Content>{modalContent || null}</Modal.Content>
        </Modal>
      )}
      <Flex $flexDirection="column" $gap={1}>
        <Text>Expanded: {String(expandState)}</Text>
        <Flex $justifyContent="center">
          <ProductGallery
            $size="small"
            $thumbnailRailPlacement="bottom"
            $aspectRatio="16 / 9"
            $navigation
            $expand
            onExpandControl={handleExpandChange}
            onItemChange={handleItemChange}
          >
            {Array.from({ length: 7 }, (_, index) => (
              <ProductGallery.Item key={index}>
                <Image alt="" role="presentation" src="https://placehold.co/1600x900" />
              </ProductGallery.Item>
            ))}
          </ProductGallery>
        </Flex>
      </Flex>
    </>
  );
}

return <Example />;

}
```

---

## Video in Gallery

When using a video inside `ProductGallery`, we recommend using the native media controls to ensure a consistent playback experience across devices. Below are two examples of possible implementations of video in the component.

**Note:** To prevent multiple videos from playing simultaneously, ensure that the currently active video is paused when switching to another item in the gallery.

```tsx
() => {
  function Example() {
    const [itemState, setItemState] = React.useState({
      ref: null
    });

    const handleItemChange = (e) => {
      if (itemState.ref && itemState.ref.current) {
        const prevVideo = itemState.ref.current.querySelector("video");
        if (prevVideo) {
          prevVideo.pause();
        }
      }

      setItemState(e);
    };

    return (
      <Flex $justifyContent="center">
        <ProductGallery
          $size="small"
          $thumbnailRailPlacement="bottom"
          $aspectRatio="16 / 9"
          $navigation
          onItemChange={handleItemChange}
          $scrollableRegionTabIndex={true}
        >
          <ProductGallery.Item thumbnailAlt="Gangs of London" thumbnailSrc="https://static.skyassets.com/contentstack/assets/blt143e20b03d72047e/blt918173bc16834e27/679a30496afeee71fcced58d/gol-s3-hero.jpg?imageManager=true&impolicy=resize&width=470">
            <Video
              controls
              src="https://static.skyassets.com/contentstack/assets/blt143e20b03d72047e/blt9e531707bd47abea/67cad71186795f6b658e84c7/GOL_RECAP_THIS_CITY_V5_REV_Online_16x9_25fps_ApprovalFile.mp4"
              playsInline
              poster="https://static.skyassets.com/contentstack/assets/blt292fe19f56d1a1a8/blt5ab2ba7395fdac40/67bef6bc53fc5e4e06b182ab/2025_Feb_Watch_Page_GOLS3_Desktop.png?imageManager=true&impolicy=resize&width=1400"
            />
          </ProductGallery.Item>

          <ProductGallery.Item thumbnailAlt="The last of us" thumbnailSrc="https://static.skyassets.com/contentstack/assets/blt143e20b03d72047e/blt1ffe2fe42f1c8d0c/63bed8ccd248bb7a0860aa1f/FL_08_The_Last_of_Us_S01_preview.jpg?imageManager=true&impolicy=resize&width=470">
            <VideoModalTrigger />
          </ProductGallery.Item>
        </ProductGallery>
      </Flex>
    );
  }

  function VideoModalTrigger() {
    const [isOpen, setIsOpen] = React.useState(false);
    const buttonRef = React.useRef();

    const toggleModal = event => {
      event.stopPropagation();
      if (isOpen && buttonRef.current) buttonRef.current.focus();
      setIsOpen(!isOpen);
    };

    return (
      <>
        {isOpen && (
          <Modal onClose={toggleModal}>
            <Modal.CloseButton />
            <Modal.Content>
              <Video
                autoPlay
                controls
                playsInline
                poster="https://static.skyassets.com/contentstack/assets/blt143e20b03d72047e/bltf438b4116de3ec2a/66f66debcbfa913acd17ccf9/tlou-s2-thumbnail.jpg?imageManager=true&impolicy=resize&width=960"
                src="https://static.skyassets.com/contentstack/assets/blt143e20b03d72047e/blt8e2a0b6c2f564f66/638a34416c16851aae1269af/TLOU_16x9_DAY_ORG.mp4"
                width="100%"
              />
            </Modal.Content>
          </Modal>
        )}

        <Box $position="relative" aria-label="Open video modal">
          <Image alt="the last of us poster" src="https://static.skyassets.com/contentstack/assets/blt143e20b03d72047e/blt58003b8446c88821/63b7fcadade3a64c6774617f/MicrosoftTeams-image_(14).png" />
          <Box $position="absolute" $bottom={spacing(2)} $left={spacing(2)}>
            <IconButton
              ref={buttonRef}
              src={playLinear}
              aria-label="Play video"
              onClick={toggleModal}
              $size="small"
            />
          </Box>
        </Box>
      </>
    );
  }

  return <Example />;
}

```

---

## System Modifiers

The `ProductGallery` component also accepts props applied using the following system modifiers:

- [margin](/core/system/margin/)

---

## Translatable Fields

The `Product Gallery` support translation on the following fields:

| Translation | Description |
| --- | --- |
| `product-gallery.expandControls.label` | aria-label for the expand control item |
| `product-gallery.navigation.next` | descriptive text for the next item action |
| `product-gallery.navigation.previous` | descriptive text for the previous item action |

For more on translatable fields, view the `useTranslation` docs here: [useTranslation](/core/hooks/use-translation/)
