Skip to main navigationSkip to main content
Return to Sky UI homepage

Product Gallery - React

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

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

<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

PropTypeDefaultDescription
$aspectRatiostring1 / 1
The aspect ratio prop can be changed to set the aspect ratio of the main image container.
$navigationbooleantrue
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 prop.
$scrollableRegionTabIndexbooleanfalse

If set, the main item container will be focusable. This is used for accessibility purposes.

Note: this prop should only be used if the the main item container has non-interactive content and $expand is set to false.

$sizestringsmall
Sets the size variant of the Product Gallery. This prop is responsive and can be used to set the size of the Product Gallery at different breakpoints.
$thumbnailRailPlacementstringbottom
The placement of the thumbnail rail. This prop is responsive and shows a preview of the other images within the gallery
$expandbooleanfalseIcon Button to maximise the main image, opening it as an overlay. It will be applied to all of the items in the gallery.
$thumbnailOutlinebooleanfalseAn outline to thumbnails that provides contrast against the background the gallery is used on.

ProductGallery.Item

PropTypeDefaultDescription
$expandbooleanfalseIcon Button to maximise the main image, opening it as an overlay. It will be applied only to item that is specified in.
thumbnailSrcstring-Sets the image source for the thumbnail of the specified item. This prop takes precedence over the child element passed into the item.
thumbnailAltstring-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.

Please view this example in fullscreen mode. Click the full screen button above.

$thumbnailRailPlacement

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

Please view this example in fullscreen mode. Click the full screen button above.

$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.

<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, 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.

Please view this example in fullscreen mode. Click the full screen button above.

Callbacks

onMount

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

() => {

  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.

() => {

 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.

() => {

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 />;

}

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.

() => {
  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:

Translatable Fields

The Product Gallery support translation on the following fields:

TranslationDescription
product-gallery.expandControls.labelaria-label for the expand control item
product-gallery.navigation.nextdescriptive text for the next item action
product-gallery.navigation.previousdescriptive text for the previous item action

For more on translatable fields, view the useTranslation docs here: useTranslation

Product Gallery - React | Sky UI Design System