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
| Prop | Type | Default | Description |
|---|---|---|---|
| $aspectRatio | string | 1 / 1 | The 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 prop. |
| $scrollableRegionTabIndex | boolean | false | 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 |
| $size | string | small | 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. |
| $thumbnailRailPlacement | string | bottom | The placement of the thumbnail rail. This prop is responsive 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.
$thumbnailRailPlacement
The $thumbnailRailPlacement prop can be set to bottom (default) or side.
$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.
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 />; }
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.
() => { 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:
| 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