---
title: "The gallery recommendation widget template"
slug: "the-gallery-recommendation-widget-template"
abstract: "Component variants and API reference of a gallery recommendation widget template in Search UI Components. Includes CLI commands and code blocks."
---
# The gallery recommendation widget template

A gallery recommendation widget displays recommendations with pagination controls.

![Screenshot of a gallery recommendation widget.](https://doc.sitecore.com/api/markdown-image/search/en/developers/search-js-sdk-for-react/images/recommendation-gallery.png)

The following sections describe the gallery recommendation widget template, and contains an API reference, and instructions on how to create code blocks that you can add to the [`WidgetsProvider`](https://doc.sitecore.com/search/en/developers/search-js-sdk-for-react/js-sdk-reference/providers/widgetsprovider.html "WidgetsProvider") in your React project using Search JS SDK for React. The widget uses the [search results query hook](https://doc.sitecore.com/search/en/developers/search-js-sdk-for-react/js-sdk-reference/query-hooks/usesearchresults.html "useSearchResults") to request and retrieve data.

To create a widget component, you can choose to use Search [UI components CLI](https://www.npmjs.com/package/@sitecore-search/cli), a command line interface, or copy and paste code blocks manually.

<details>
<summary><h2>Generate or create the component</h2></summary>

This component is available in the plain or unstyled, styled, CSS-styled, and CSS module-styled variants. The following sections are instructions to generate or manually create the component in Typescript and JavaScript.

> [!important]
> Remember to adjust the entity model in the widget template to match your implementation.

### Plain component

Plain widget templates are unstyled and ready to be customized with CSS styles or a theme. You have complete control over the design and content of the component.

#### TS CLI { .notintoc }

With the Sitecore Search CLI you can replicate all necessary files of the widget template in your React project. The following procedure includes instructions for installing the CLI in your project and specifying the location where widget components are created.

If you have previously installed the CLI in your project and have a location defined in a file called `.sc-search-settings.json`, you can proceed directly to step 3.

To install the UI Components CLI in your project:

1. To install the Search UI Components CLI, open the terminal of your IDE or command prompt and run the following command:

    ```shell
    npx install --save-dev @sitecore-search/cli
    ```

2. To define the location of where widget components will be created, at the root of your project, in a file called `.sc-search-settings.json`, specify the location as shown in the following code block.

    ```js
    {
        "components-path": "src-test/components"
    }
    ```

3. To create the widget component, go to the location you defined in the `.sc-search-settings.json` file then open the terminal of your IDE or command prompt and run the following command:

    ```shell
    npx sc-search new-widget --language typescript --template recommendation-gallery --entity content --styling plain
    ```

#### TS manual { .notintoc }

When you manually create widget templates in your React project, you need to ensure that you install any required dependencies and create all necessary files.

if you have previously installed the required NPM packages, you can skip the dependency installation steps.

To create the widget component using this widget template in your project:

1. In your project, to install the development dependency, open the terminal of your IDE or command prompt and run the following command:

    ```shell
    npm install @radix-ui/react-icons@^1.3.0
    ```

2. In a folder for the widget component, create a file called `index.tsx` then in the file, paste following code block.

    ```typescript
    // index.tsx
    import type { KeyboardEvent } from 'react';
    import { useCallback, useEffect, useRef, useState } from 'react';

    import { ArrowLeftIcon, ArrowRightIcon } from '@radix-ui/react-icons';
    import type { RecommendationInitialState } from '@sitecore-search/react';
    import { WidgetDataType, useRecommendation, widget } from '@sitecore-search/react';
    import { ArticleCard, Presence } from '@sitecore-search/ui';

    const KeyboardKeys = {
      RIGHT: 'ArrowRight',
      LEFT: 'ArrowLeft',
    };
    const label = 'Recommendation Gallery';
    const previousLabel = 'Previous';
    const nextLabel = 'Next';
    type ArticleModel = {
      id: string;
      name: string;
      author?: string;
      url?: string;
      image_url?: string;
      source_id?: string;
    };
    type ArticleGalleryProps = {
      autoplay?: boolean;
      autoplaySpeed?: number;
      displayIndicators?: boolean;
      title?: string;
      itemsToDisplay?: number;
    };
    type InitialState = RecommendationInitialState<'itemsPerPage'>;
    const DEFAULT_IMG_URL = 'https://placehold.co/500x300?text=No%20Image'; // TODO: Update with corresponding fallback image

    export const RecommendationGalleryComponent = ({
      autoplay = false,
      autoplaySpeed = 5000,
      displayIndicators = false,
      title = '',
      itemsToDisplay = 6,
    }: ArticleGalleryProps) => {
      const {
        widgetRef,
        actions: { onNavigationNext, onNavigationPrev, onItemClick },
        queryResult: { isLoading, isFetching, data: { content: articles = [] } = {} },
      } = useRecommendation<ArticleModel, InitialState>({
        state: {
          itemsPerPage: itemsToDisplay,
        },
      });
      const totalItems = articles.length;
      const prevActionRef = useRef<HTMLButtonElement>(null);
      const nextActionRef = useRef<HTMLButtonElement>(null);
      const [selectedIndex, setSelectedIndex] = useState(0);
      const goTo = useCallback(
        (index: number) => {
          if (selectedIndex === index) return;
          setSelectedIndex((current) => {
            if (index > current) {
              onNavigationNext({
                index,
              });
            } else {
              onNavigationPrev({
                index,
              });
            }
            return index;
          });
        },
        [selectedIndex, onNavigationNext, onNavigationPrev],
      );
      const handleNext = useCallback(() => {
        setSelectedIndex((current) => {
          const newIndex = current < totalItems - 1 ? current + 1 : 0;
          onNavigationNext({
            index: newIndex,
          });
          return newIndex;
        });
      }, [totalItems, onNavigationNext]);
      const handlePrevious = useCallback(() => {
        setSelectedIndex((current) => {
          const newIndex = current > 0 ? current - 1 : totalItems - 1;
          onNavigationPrev({
            index: newIndex,
          });
          return newIndex;
        });
      }, [totalItems, onNavigationPrev]);
      const handleKeyDown = useCallback(
        (event: KeyboardEvent) => {
          switch (event.key) {
            case KeyboardKeys.RIGHT: {
              event.preventDefault();
              nextActionRef?.current?.focus();
              handleNext();
              break;
            }
            case KeyboardKeys.LEFT: {
              event.preventDefault();
              prevActionRef?.current?.focus();
              handlePrevious();
              break;
            }
            default:
              break;
          }
        },
        [handlePrevious, handleNext],
      );
      useEffect(() => {
        let interval: ReturnType<typeof setInterval>;
        if (autoplay && totalItems) {
          interval = setInterval(handleNext, autoplaySpeed);
        }
        return () => interval && clearInterval(interval);
      }, [totalItems]);
      const loading = isLoading || isFetching;
      return (
        <div>
          <Presence present={loading}>
            <div>
              <svg aria-busy={loading} aria-hidden={!loading} focusable="false" role="progressbar" viewBox="0 0 20 20">
                <path d="M7.229 1.173a9.25 9.25 0 1 0 11.655 11.412 1.25 1.25 0 1 0-2.4-.698 6.75 6.75 0 1 1-8.506-8.329 1.25 1.25 0 1 0-.75-2.385z" />
              </svg>
            </div>
          </Presence>
          {!loading && articles.length > 0 && (
            <>
              {title && <h3>{title}</h3>}
              <section aria-roledescription="carousel" aria-label={label} onKeyDown={handleKeyDown} ref={widgetRef}>
                <button
                  onClick={handlePrevious}
                  data-direction="left"
                  ref={prevActionRef}
                  aria-label={previousLabel}
                  type="button"
                >
                  <ArrowLeftIcon />
                </button>

                <button
                  onClick={handleNext}
                  data-direction="right"
                  ref={nextActionRef}
                  aria-label={nextLabel}
                  type="button"
                >
                  <ArrowRightIcon />
                </button>

                <div aria-live={autoplay ? 'off' : 'polite'}>
                  {articles.map((item, index) => (
                    <div
                      role="group"
                      aria-roledescription="slide"
                      key={item.id}
                      data-state={index === selectedIndex ? 'selected' : ''}
                      aria-label={`${index + 1} of ${totalItems} slides`}
                    >
                      <ArticleCard.Root key={item.id}>
                        <div>
                          <ArticleCard.Image src={item?.image_url || DEFAULT_IMG_URL} />
                        </div>
                        <div>
                          <a
                            href={item.url}
                            onClick={(event) => {
                              event.preventDefault();
                              onItemClick({
                                id: item.id,
                                index,
                                sourceId: item.source_id,
                              });
                            }}
                          >
                            <ArticleCard.Title>{item.name}</ArticleCard.Title>
                            <ArticleCard.Subtitle>{item.author}</ArticleCard.Subtitle>
                          </a>
                        </div>
                      </ArticleCard.Root>
                    </div>
                  ))}
                </div>

                {displayIndicators && (
                  <div>
                    {articles.map((p, index) => (
                      <button
                        key={p.id}
                        data-slide-to={index}
                        aria-label={`Slide ${index + 1}`}
                        data-state={index === selectedIndex ? 'active' : ''}
                        onClick={() => goTo(index)}
                        type="button"
                      />
                    ))}
                  </div>
                )}
              </section>
            </>
          )}
        </div>
      );
    };
    const RecommendationGalleryWidget = widget(RecommendationGalleryComponent, WidgetDataType.RECOMMENDATION, 'content');
    export default RecommendationGalleryWidget;
    ```

#### JS CLI { .notintoc }

With the Sitecore Search CLI you can replicate all necessary files of the widget template in your React project. The following procedure includes instructions for installing the CLI in your project and specifying the location where widget components are created.

If you have previously installed the CLI in your project and have a location defined in a file called `.sc-search-settings.json`, you can proceed directly to step 3.

To install the UI Components CLI in your project:

1. To install the Search UI Components CLI, open the terminal of your IDE or command prompt and run the following command:

    ```shell
    npx install --save-dev @sitecore-search/cli
    ```

2. To define the location of where widget components will be created, at the root of your project, in a file called `.sc-search-settings.json`, specify the location as shown in the following code block.

    ```js
    {
        "components-path": "src-test/components"
    }
    ```

3. At the location listed in `.sc-search-settings.json`, to create the widget component in an `index.jsx` file, paste the following command in the terminal of your IDE.

    ```shell
    npx sc-search new-widget --language javascript --template recommendation-gallery --entity content --styling plain
    ```

#### JS manual { .notintoc }

When you manually create widget templates in your React project, you need to ensure that you install any required dependencies and create all necessary files.

if you have previously installed the required NPM packages, you can skip the dependency installation steps.

To create the widget component using this widget template in your project:

1. In your project, to install the development dependency, open the terminal of your IDE or command prompt and run the following command:

    ```shell
    npm install @radix-ui/react-icons@^1.3.0
    ```

2. In a folder for the widget component, create a file called `index.jsx` then in the file, paste following code block.

    ```javascript
    // index.jsx
    import { useCallback, useEffect, useRef, useState } from 'react';

    import { ArrowLeftIcon, ArrowRightIcon } from '@radix-ui/react-icons';
    import { WidgetDataType, useRecommendation, widget } from '@sitecore-search/react';
    import { ArticleCard, Presence } from '@sitecore-search/ui';

    const KeyboardKeys = {
      RIGHT: 'ArrowRight',
      LEFT: 'ArrowLeft',
    };
    const label = 'Recommendation Gallery';
    const previousLabel = 'Previous';
    const nextLabel = 'Next';
    const DEFAULT_IMG_URL = 'https://placehold.co/500x300?text=No%20Image'; // TODO: Update with corresponding fallback image

    export const RecommendationGalleryComponent = ({
      autoplay = false,
      autoplaySpeed = 5000,
      displayIndicators = false,
      title = '',
      itemsToDisplay = 6,
    }) => {
      const {
        widgetRef,
        actions: { onNavigationNext, onNavigationPrev, onItemClick },
        queryResult: { isLoading, isFetching, data: { content: articles = [] } = {} },
      } = useRecommendation({
        state: {
          itemsPerPage: itemsToDisplay,
        },
      });
      const totalItems = articles.length;
      const prevActionRef = useRef(null);
      const nextActionRef = useRef(null);
      const [selectedIndex, setSelectedIndex] = useState(0);
      const goTo = useCallback(
        (index) => {
          if (selectedIndex === index) return;
          setSelectedIndex((current) => {
            if (index > current) {
              onNavigationNext({
                index,
              });
            } else {
              onNavigationPrev({
                index,
              });
            }
            return index;
          });
        },
        [selectedIndex, onNavigationNext, onNavigationPrev],
      );
      const handleNext = useCallback(() => {
        setSelectedIndex((current) => {
          const newIndex = current < totalItems - 1 ? current + 1 : 0;
          onNavigationNext({
            index: newIndex,
          });
          return newIndex;
        });
      }, [totalItems, onNavigationNext]);
      const handlePrevious = useCallback(() => {
        setSelectedIndex((current) => {
          const newIndex = current > 0 ? current - 1 : totalItems - 1;
          onNavigationPrev({
            index: newIndex,
          });
          return newIndex;
        });
      }, [totalItems, onNavigationPrev]);
      const handleKeyDown = useCallback(
        (event) => {
          switch (event.key) {
            case KeyboardKeys.RIGHT: {
              event.preventDefault();
              nextActionRef?.current?.focus();
              handleNext();
              break;
            }
            case KeyboardKeys.LEFT: {
              event.preventDefault();
              prevActionRef?.current?.focus();
              handlePrevious();
              break;
            }
            default:
              break;
          }
        },
        [handlePrevious, handleNext],
      );
      useEffect(() => {
        let interval;
        if (autoplay && totalItems) {
          interval = setInterval(handleNext, autoplaySpeed);
        }
        return () => interval && clearInterval(interval);
      }, [totalItems]);
      const loading = isLoading || isFetching;
      return (
        <div>
          <Presence present={loading}>
            <div>
              <svg aria-busy={loading} aria-hidden={!loading} focusable="false" role="progressbar" viewBox="0 0 20 20">
                <path d="M7.229 1.173a9.25 9.25 0 1 0 11.655 11.412 1.25 1.25 0 1 0-2.4-.698 6.75 6.75 0 1 1-8.506-8.329 1.25 1.25 0 1 0-.75-2.385z" />
              </svg>
            </div>
          </Presence>
          {!loading && articles.length > 0 && (
            <>
              {title && <h3>{title}</h3>}
              <section aria-roledescription="carousel" aria-label={label} onKeyDown={handleKeyDown} ref={widgetRef}>
                <button
                  onClick={handlePrevious}
                  data-direction="left"
                  ref={prevActionRef}
                  aria-label={previousLabel}
                  type="button"
                >
                  <ArrowLeftIcon />
                </button>

                <button
                  onClick={handleNext}
                  data-direction="right"
                  ref={nextActionRef}
                  aria-label={nextLabel}
                  type="button"
                >
                  <ArrowRightIcon />
                </button>

                <div aria-live={autoplay ? 'off' : 'polite'}>
                  {articles.map((item, index) => (
                    <div
                      role="group"
                      aria-roledescription="slide"
                      key={item.id}
                      data-state={index === selectedIndex ? 'selected' : ''}
                      aria-label={`${index + 1} of ${totalItems} slides`}
                    >
                      <ArticleCard.Root key={item.id}>
                        <div>
                          <ArticleCard.Image src={item?.image_url || DEFAULT_IMG_URL} />
                        </div>
                        <div>
                          <a
                            href={item.url}
                            onClick={(event) => {
                              event.preventDefault();
                              onItemClick({
                                id: item.id,
                                index,
                                sourceId: item.source_id,
                              });
                            }}
                          >
                            <ArticleCard.Title>{item.name}</ArticleCard.Title>
                            <ArticleCard.Subtitle>{item.author}</ArticleCard.Subtitle>
                          </a>
                        </div>
                      </ArticleCard.Root>
                    </div>
                  ))}
                </div>

                {displayIndicators && (
                  <div>
                    {articles.map((p, index) => (
                      <button
                        key={p.id}
                        data-slide-to={index}
                        aria-label={`Slide ${index + 1}`}
                        data-state={index === selectedIndex ? 'active' : ''}
                        onClick={() => goTo(index)}
                        type="button"
                      />
                    ))}
                  </div>
                )}
              </section>
            </>
          )}
        </div>
      );
    };
    const RecommendationGalleryWidget = widget(RecommendationGalleryComponent, WidgetDataType.RECOMMENDATION, 'content');
    export default RecommendationGalleryWidget;
    ```

### Styled component

Styled widget templates are built with styled UI primitives. To customize them, you need to edit the styled component. You can control the design and content of the component in the `styled` file.

#### TS CLI { .notintoc }

With the Sitecore Search CLI you can replicate all necessary files of the widget template in your React project. The following procedure includes instructions for installing the CLI in your project and specifying the location where widget components are created.

If you have previously installed the CLI in your project and have a location defined in a file called `.sc-search-settings.json`, you can proceed directly to step 3.

To install the UI Components CLI in your project:

1. To install the Search UI Components CLI, open the terminal of your IDE or command prompt and run the following command:

    ```shell
    npx install --save-dev @sitecore-search/cli
    ```

2. To define the location of where widget components will be created, at the root of your project, in a file called `.sc-search-settings.json`, specify the location as shown in the following code block.

    ```js
    {
        "components-path": "src-test/components"
    }
    ```

3. To create the widget component in `index.tsx` and `styled.ts` files, go to the location you defined in the `.sc-search-settings.json` file then open the terminal of your IDE or command prompt and run the following command:

    ```shell
    npx sc-search new-widget --language typescript --template recommendation-gallery --entity content --styling styled
    ```

#### TS manual { .notintoc }

When you manually create widget templates in your React project, you need to ensure that you install any required dependencies and create all necessary files.

if you have previously installed the required NPM packages, you can skip the dependency installation steps.

To create the widget component using this widget template in your project:

1. In your project, to install development dependencies, open the terminal of your IDE or command prompt and run the following command:

    ```shell
    npm install styled-components@^6.1.0 @radix-ui/react-icons@^1.3.0
    ```

2. In your project, to install the development dependency, open the terminal of your IDE or command prompt and run the following command:

    ```shell
    npm install --save-dev @types/styled-components@^5.1.29
    ```

3. In a folder for the widget component, create files called `index.tsx` and `styled.ts` then in respective files, paste following code blocks.

    ```typescript
    // index.tsx
    import type { KeyboardEvent } from 'react';
    import { useCallback, useEffect, useRef, useState } from 'react';

    import { ArrowLeftIcon, ArrowRightIcon } from '@radix-ui/react-icons';
    import type { RecommendationInitialState } from '@sitecore-search/react';
    import { WidgetDataType, useRecommendation, widget } from '@sitecore-search/react';
    import { Presence } from '@sitecore-search/ui';

    import {
      ArticleCardStyled,
      GalleryAction,
      GalleryGrid,
      GalleryIndicator,
      GalleryIndicators,
      GallerySlideWrapper,
      GallerySlides,
      LoaderAnimation,
      LoaderContainer,
      RecommendationContainer,
      TitleStyled,
    } from './styled';

    const KeyboardKeys = {
      RIGHT: 'ArrowRight',
      LEFT: 'ArrowLeft',
    };

    const label = 'Recommendation Gallery';
    const previousLabel = 'Previous';
    const nextLabel = 'Next';

    type ArticleModel = {
      id: string;
      name: string;
      author?: string;
      url?: string;
      image_url?: string;
      source_id?: string;
    };

    type ArticleGalleryProps = {
      autoplay?: boolean;
      autoplaySpeed?: number;
      displayIndicators?: boolean;
      title?: string;
      itemsToDisplay?: number;
    };

    type InitialState = RecommendationInitialState<'itemsPerPage'>;

    const DEFAULT_IMG_URL = 'https://placehold.co/500x300?text=No%20Image'; // TODO: Update with corresponding fallback image

    export const RecommendationGalleryComponent = ({
      autoplay = false,
      autoplaySpeed = 5000,
      displayIndicators = false,
      title = '',
      itemsToDisplay = 6,
    }: ArticleGalleryProps) => {
      const {
        widgetRef,
        actions: { onNavigationNext, onNavigationPrev, onItemClick },
        queryResult: { isLoading, isFetching, data: { content: articles = [] } = {} },
      } = useRecommendation<ArticleModel, InitialState>({
        state: {
          itemsPerPage: itemsToDisplay,
        },
      });

      const totalItems = articles.length;

      const prevActionRef = useRef<HTMLButtonElement>(null);
      const nextActionRef = useRef<HTMLButtonElement>(null);
      const [selectedIndex, setSelectedIndex] = useState(0);

      const goTo = useCallback(
        (index: number) => {
          if (selectedIndex === index) return;

          setSelectedIndex((current) => {
            if (index > current) {
              onNavigationNext({ index });
            } else {
              onNavigationPrev({ index });
            }
            return index;
          });
        },
        [selectedIndex, onNavigationNext, onNavigationPrev],
      );

      const handleNext = useCallback(() => {
        setSelectedIndex((current) => {
          const newIndex = current < totalItems - 1 ? current + 1 : 0;

          onNavigationNext({ index: newIndex });
          return newIndex;
        });
      }, [totalItems, onNavigationNext]);

      const handlePrevious = useCallback(() => {
        setSelectedIndex((current) => {
          const newIndex = current > 0 ? current - 1 : totalItems - 1;
          onNavigationPrev({ index: newIndex });
          return newIndex;
        });
      }, [totalItems, onNavigationPrev]);

      const handleKeyDown = useCallback(
        (event: KeyboardEvent) => {
          switch (event.key) {
            case KeyboardKeys.RIGHT: {
              event.preventDefault();
              nextActionRef?.current?.focus();
              handleNext();
              break;
            }

            case KeyboardKeys.LEFT: {
              event.preventDefault();
              prevActionRef?.current?.focus();
              handlePrevious();
              break;
            }

            default:
              break;
          }
        },
        [handlePrevious, handleNext],
      );

      useEffect(() => {
        let interval: ReturnType<typeof setInterval>;

        if (autoplay && totalItems) {
          interval = setInterval(handleNext, autoplaySpeed);
        }
        return () => interval && clearInterval(interval);
      }, [totalItems]);

      const loading = isLoading || isFetching;

      return (
        <RecommendationContainer>
          <Presence present={loading}>
            <LoaderContainer>
              <LoaderAnimation
                aria-busy={loading}
                aria-hidden={!loading}
                focusable="false"
                role="progressbar"
                viewBox="0 0 20 20"
              >
                <path d="M7.229 1.173a9.25 9.25 0 1 0 11.655 11.412 1.25 1.25 0 1 0-2.4-.698 6.75 6.75 0 1 1-8.506-8.329 1.25 1.25 0 1 0-.75-2.385z" />
              </LoaderAnimation>
            </LoaderContainer>
          </Presence>
          {!loading && articles.length > 0 && (
            <>
              {title && <TitleStyled>{title}</TitleStyled>}
              <GalleryGrid aria-roledescription="carousel" aria-label={label} onKeyDown={handleKeyDown} ref={widgetRef}>
                <GalleryAction
                  onClick={handlePrevious}
                  data-direction="left"
                  ref={prevActionRef}
                  aria-label={previousLabel}
                  type="button"
                >
                  <ArrowLeftIcon />
                </GalleryAction>

                <GalleryAction
                  onClick={handleNext}
                  data-direction="right"
                  ref={nextActionRef}
                  aria-label={nextLabel}
                  type="button"
                >
                  <ArrowRightIcon />
                </GalleryAction>

                <GallerySlides aria-live={autoplay ? 'off' : 'polite'}>
                  {articles.map((item, index) => (
                    <GallerySlideWrapper
                      role="group"
                      aria-roledescription="slide"
                      key={item.id}
                      data-state={index === selectedIndex ? 'selected' : ''}
                      aria-label={`${index + 1} of ${totalItems} slides`}
                    >
                      <ArticleCardStyled.Root key={item.id}>
                        <ArticleCardStyled.ImageWrapper>
                          <ArticleCardStyled.Image src={item?.image_url || DEFAULT_IMG_URL} />
                        </ArticleCardStyled.ImageWrapper>
                        <ArticleCardStyled.Content>
                          <ArticleCardStyled.Link
                            href={item.url}
                            onClick={(event) => {
                              event.preventDefault();
                              onItemClick({ id: item.id, index, sourceId: item.source_id });
                            }}
                          >
                            <ArticleCardStyled.Title>{item.name}</ArticleCardStyled.Title>
                            <ArticleCardStyled.Subtitle>{item.author}</ArticleCardStyled.Subtitle>
                          </ArticleCardStyled.Link>
                        </ArticleCardStyled.Content>
                      </ArticleCardStyled.Root>
                    </GallerySlideWrapper>
                  ))}
                </GallerySlides>

                {displayIndicators && (
                  <GalleryIndicators>
                    {articles.map((p, index) => (
                      <GalleryIndicator
                        key={p.id}
                        data-slide-to={index}
                        aria-label={`Slide ${index + 1}`}
                        data-state={index === selectedIndex ? 'active' : ''}
                        onClick={() => goTo(index)}
                        type="button"
                      />
                    ))}
                  </GalleryIndicators>
                )}
              </GalleryGrid>
            </>
          )}
        </RecommendationContainer>
      );
    };

    const RecommendationGalleryWidget = widget(RecommendationGalleryComponent, WidgetDataType.RECOMMENDATION, 'content');
    export default RecommendationGalleryWidget;
    ```

    ```typescript
    // styled.ts
    import { keyframes, styled } from 'styled-components';

    import { ArticleCard, theme } from '@sitecore-search/ui';

    // Article
    const ArticleCardRootStyled = styled(ArticleCard.Root)`
      cursor: pointer;
      box-sizing: border-box;
      box-shadow: 0 0 2px 2px ${theme.vars.palette.grey['400']};
      background: #fff;
      position: relative;
      -webkit-transition: 0.3s all ease;
      transition: 0.15s all ease;
      width: 250px;

      &:focus-within {
        box-shadow: 0 0 8px 3px rgba(0, 0, 0, 0.15);
        transform: scale(1.025);
        opacity: 1;
        transition: 0.35s all ease;
      }

      &:hover {
        box-shadow: 0 0 8px 3px rgba(0, 0, 0, 0.15);
        transform: scale(1.025);
        opacity: 1;
        transition: 0.35s all ease;
      }
    `;

    const ArticleCardContentStyled = styled.div`
      display: flex;
      height: 150px;
      overflow: hidden;
      padding: ${theme.vars.spacing.m};
    `;

    const ArticleCardLinkStyled = styled.a`
      color: ${theme.vars.palette.text.primary};
      font-size: ${theme.vars.typography.fontSize3.fontSize};
      text-decoration: none;

      &:hover {
        text-decoration: none;
      }

      &:focus {
        text-decoration: none;
      }
      &::after {
        position: absolute;
        inset: 0;
        display: block;
        content: ' ';
      }
    `;

    const ArticleCardImageStyled = styled(ArticleCard.Image)`
      width: 100%;
    `;

    const ArticleCardImageWrapperStyled = styled.div`
      position: relative;
      height: 120px;
      display: flex;
      justify-content: center;
      align-items: center;
      overflow: hidden;
    `;

    const ArticleCardTitleStyled = styled(ArticleCard.Title)`
      color: ${theme.vars.palette.text.primary};
      font-family: ${theme.vars.typography.fontFamilySystem};
      font-size: ${theme.vars.typography.h3.fontSize};
      font-weight: ${theme.vars.typography.h3.fontWeight};
      line-height: ${theme.vars.typography.h3.lineHeight};
      margin: 0 0 ${theme.vars.spacing.m};
      overflow: hidden;
      display: -webkit-box;
      -webkit-line-clamp: 3; /* number of lines to show */
      -webkit-box-orient: vertical;
    `;

    const ArticleCardSubtitleStyled = styled(ArticleCard.Subtitle)`
      color: ${theme.vars.palette.text.secondary};
      font-family: ${theme.vars.typography.fontFamilySystem};
      font-size: ${theme.vars.typography.fontSize3.fontSize};
      font-weight: ${theme.vars.typography.fontSize3.fontWeight};
      line-height: ${theme.vars.typography.fontSize3.lineHeight};
      margin: 0 0 ${theme.vars.spacing.m};
    `;

    export const ArticleCardStyled = {
      Root: ArticleCardRootStyled,
      Content: ArticleCardContentStyled,
      Link: ArticleCardLinkStyled,
      Image: ArticleCardImageStyled,
      ImageWrapper: ArticleCardImageWrapperStyled,
      Title: ArticleCardTitleStyled,
      Subtitle: ArticleCardSubtitleStyled,
    };

    // misc
    export const TitleStyled = styled.h3`
      color: ${theme.vars.palette.primary.main};
      font-family: ${theme.vars.typography.fontFamilySystem};
      font-size: ${theme.vars.typography.fontSize3.fontSize};
    `;

    export const LoaderContainer = styled.div`
      align-items: center;
      display: flex;
      min-height: 50vh;
    `;

    const Rotate = keyframes`
      from {
        transform: rotate(0deg);
      }
      to {
        transform: rotate(360deg);
      }
    `;

    export const LoaderAnimation = styled.svg`
      animation: ${Rotate} 2s linear infinite;
      display: block;
      fill: ${theme.vars.palette.primary.main};
      height: 50px;
      margin: auto;
      width: 50px;
    `;

    export const GalleryGrid = styled.section`
      display: grid;
      grid-template-columns: auto 1fr auto;
      grid-template-areas:
        'startAction slides endAction'
        'indicators indicators indicators';
      column-gap: 10px;
      position: relative;
    `;

    export const GallerySlides = styled.div`
      grid-area: slides;
    `;

    export const GalleryAction = styled.button`
      align-self: center;
      background-color: ${theme.vars.palette.common.black};
      border: none;
      border-radius: 50%;
      color: ${theme.vars.palette.common.white};
      cursor: pointer;
      margin: 0;
      padding: 0;
      width: 50px;
      height: 50px;

      &[data-direction='right'] {
        grid-area: endAction;
      }

      &[data-direction='left'] {
        grid-area: startAction;
      }

      &:focus,
      &:hover {
        background-color: ${theme.vars.palette.primary.main};
        color: ${theme.vars.palette.primary.contrastText};
      }
    `;

    export const GallerySlideWrapper = styled.div`
      display: none;
      justify-content: center;

      &[data-state='selected'] {
        display: flex;
      }
    `;

    export const GalleryIndicators = styled.div`
      display: flex;
      grid-area: indicators;
      justify-content: center;
      margin-top: ${theme.vars.spacing.m};
      padding-left: 0;
    `;

    export const GalleryIndicator = styled.button`
      border: none;
      position: relative;
      flex: 0 1 auto;
      width: 15px;
      height: 3px;
      margin-right: ${theme.vars.spacing.s};
      margin-left: ${theme.vars.spacing.s};
      text-indent: -999px;
      cursor: pointer;
      background-color: ${theme.vars.palette.common.black};

      &[data-state='active'] {
        background-color: ${theme.vars.palette.primary.main};
      }

      &::before {
        position: absolute;
        top: -10px;
        left: 0;
        display: inline-block;
        width: 100%;
        height: 10px;
        content: '';
      }

      &::after {
        position: absolute;
        bottom: -10px;
        left: 0;
        display: inline-block;
        width: 100%;
        height: 10px;
        content: '';
      }
    `;

    export const RecommendationContainer = styled.div`
      display: inline-block;
      width: auto;
      min-width: 300px;
    `;
    ```

#### JS CLI { .notintoc }

With the Sitecore Search CLI you can replicate all necessary files of the widget template in your React project. The following procedure includes instructions for installing the CLI in your project and specifying the location where widget components are created.

If you have previously installed the CLI in your project and have a location defined in a file called `.sc-search-settings.json`, you can proceed directly to step 3.

To install the UI Components CLI in your project:

1. To install the Search UI Components CLI, open the terminal of your IDE or command prompt and run the following command:

    ```shell
    npx install --save-dev @sitecore-search/cli
    ```

2. To define the location of where widget components will be created, at the root of your project, in a file called `.sc-search-settings.json`, specify the location as shown in the following code block.

    ```js
    {
        "components-path": "src-test/components"
    }
    ```

3. To create the widget component in `index.jsx` and `styled.js` files, go to the location you defined in the `.sc-search-settings.json` file then open the terminal of your IDE or command prompt and run the following command:

    ```shell
    npx sc-search new-widget --language javascript --template recommendation-gallery --entity content --styling styled
    ```

#### JS manual { .notintoc }

When you manually create widget templates in your React project, you need to ensure that you install any required dependencies and create all necessary files.

if you have previously installed the required NPM packages, you can skip the dependency installation steps.

To create the widget component using this widget template in your project:

1. In your project, to install development dependencies, open the terminal of your IDE or command prompt and run the following command:

    ```shell
    npm install styled-components@^6.1.0 @radix-ui/react-icons@^1.3.0
    ```

2. In your project, to install the development dependency, open the terminal of your IDE or command prompt and run the following command:

    ```shell
    npm install --save-dev @types/styled-components@^5.1.29
    ```

3. In a folder for the widget component, create files called `index.jsx` and `styled.js` then in respective files, paste following code blocks.

    ```javascript
    // index.jsx
    import { useCallback, useEffect, useRef, useState } from 'react';

    import { ArrowLeftIcon, ArrowRightIcon } from '@radix-ui/react-icons';
    import { WidgetDataType, useRecommendation, widget } from '@sitecore-search/react';
    import { Presence } from '@sitecore-search/ui';

    import {
      ArticleCardStyled,
      GalleryAction,
      GalleryGrid,
      GalleryIndicator,
      GalleryIndicators,
      GallerySlideWrapper,
      GallerySlides,
      LoaderAnimation,
      LoaderContainer,
      RecommendationContainer,
      TitleStyled,
    } from './styled';

    const KeyboardKeys = {
      RIGHT: 'ArrowRight',
      LEFT: 'ArrowLeft',
    };
    const label = 'Recommendation Gallery';
    const previousLabel = 'Previous';
    const nextLabel = 'Next';
    const DEFAULT_IMG_URL = 'https://placehold.co/500x300?text=No%20Image'; // TODO: Update with corresponding fallback image

    export const RecommendationGalleryComponent = ({
      autoplay = false,
      autoplaySpeed = 5000,
      displayIndicators = false,
      title = '',
      itemsToDisplay = 6,
    }) => {
      const {
        widgetRef,
        actions: { onNavigationNext, onNavigationPrev, onItemClick },
        queryResult: { isLoading, isFetching, data: { content: articles = [] } = {} },
      } = useRecommendation({
        state: {
          itemsPerPage: itemsToDisplay,
        },
      });
      const totalItems = articles.length;
      const prevActionRef = useRef(null);
      const nextActionRef = useRef(null);
      const [selectedIndex, setSelectedIndex] = useState(0);
      const goTo = useCallback(
        (index) => {
          if (selectedIndex === index) return;
          setSelectedIndex((current) => {
            if (index > current) {
              onNavigationNext({
                index,
              });
            } else {
              onNavigationPrev({
                index,
              });
            }
            return index;
          });
        },
        [selectedIndex, onNavigationNext, onNavigationPrev],
      );
      const handleNext = useCallback(() => {
        setSelectedIndex((current) => {
          const newIndex = current < totalItems - 1 ? current + 1 : 0;
          onNavigationNext({
            index: newIndex,
          });
          return newIndex;
        });
      }, [totalItems, onNavigationNext]);
      const handlePrevious = useCallback(() => {
        setSelectedIndex((current) => {
          const newIndex = current > 0 ? current - 1 : totalItems - 1;
          onNavigationPrev({
            index: newIndex,
          });
          return newIndex;
        });
      }, [totalItems, onNavigationPrev]);
      const handleKeyDown = useCallback(
        (event) => {
          switch (event.key) {
            case KeyboardKeys.RIGHT: {
              event.preventDefault();
              nextActionRef?.current?.focus();
              handleNext();
              break;
            }
            case KeyboardKeys.LEFT: {
              event.preventDefault();
              prevActionRef?.current?.focus();
              handlePrevious();
              break;
            }
            default:
              break;
          }
        },
        [handlePrevious, handleNext],
      );
      useEffect(() => {
        let interval;
        if (autoplay && totalItems) {
          interval = setInterval(handleNext, autoplaySpeed);
        }
        return () => interval && clearInterval(interval);
      }, [totalItems]);
      const loading = isLoading || isFetching;
      return (
        <RecommendationContainer>
          <Presence present={loading}>
            <LoaderContainer>
              <LoaderAnimation
                aria-busy={loading}
                aria-hidden={!loading}
                focusable="false"
                role="progressbar"
                viewBox="0 0 20 20"
              >
                <path d="M7.229 1.173a9.25 9.25 0 1 0 11.655 11.412 1.25 1.25 0 1 0-2.4-.698 6.75 6.75 0 1 1-8.506-8.329 1.25 1.25 0 1 0-.75-2.385z" />
              </LoaderAnimation>
            </LoaderContainer>
          </Presence>
          {!loading && articles.length > 0 && (
            <>
              {title && <TitleStyled>{title}</TitleStyled>}
              <GalleryGrid aria-roledescription="carousel" aria-label={label} onKeyDown={handleKeyDown} ref={widgetRef}>
                <GalleryAction
                  onClick={handlePrevious}
                  data-direction="left"
                  ref={prevActionRef}
                  aria-label={previousLabel}
                  type="button"
                >
                  <ArrowLeftIcon />
                </GalleryAction>

                <GalleryAction
                  onClick={handleNext}
                  data-direction="right"
                  ref={nextActionRef}
                  aria-label={nextLabel}
                  type="button"
                >
                  <ArrowRightIcon />
                </GalleryAction>

                <GallerySlides aria-live={autoplay ? 'off' : 'polite'}>
                  {articles.map((item, index) => (
                    <GallerySlideWrapper
                      role="group"
                      aria-roledescription="slide"
                      key={item.id}
                      data-state={index === selectedIndex ? 'selected' : ''}
                      aria-label={`${index + 1} of ${totalItems} slides`}
                    >
                      <ArticleCardStyled.Root key={item.id}>
                        <ArticleCardStyled.ImageWrapper>
                          <ArticleCardStyled.Image src={item?.image_url || DEFAULT_IMG_URL} />
                        </ArticleCardStyled.ImageWrapper>
                        <ArticleCardStyled.Content>
                          <ArticleCardStyled.Link
                            href={item.url}
                            onClick={(event) => {
                              event.preventDefault();
                              onItemClick({
                                id: item.id,
                                index,
                                sourceId: item.source_id,
                              });
                            }}
                          >
                            <ArticleCardStyled.Title>{item.name}</ArticleCardStyled.Title>
                            <ArticleCardStyled.Subtitle>{item.author}</ArticleCardStyled.Subtitle>
                          </ArticleCardStyled.Link>
                        </ArticleCardStyled.Content>
                      </ArticleCardStyled.Root>
                    </GallerySlideWrapper>
                  ))}
                </GallerySlides>

                {displayIndicators && (
                  <GalleryIndicators>
                    {articles.map((p, index) => (
                      <GalleryIndicator
                        key={p.id}
                        data-slide-to={index}
                        aria-label={`Slide ${index + 1}`}
                        data-state={index === selectedIndex ? 'active' : ''}
                        onClick={() => goTo(index)}
                        type="button"
                      />
                    ))}
                  </GalleryIndicators>
                )}
              </GalleryGrid>
            </>
          )}
        </RecommendationContainer>
      );
    };
    const RecommendationGalleryWidget = widget(RecommendationGalleryComponent, WidgetDataType.RECOMMENDATION, 'content');
    export default RecommendationGalleryWidget;
    ```

    ```javascript
    // styled.js
    import { keyframes, styled } from 'styled-components';

    import { ArticleCard, theme } from '@sitecore-search/ui';

    // Article
    const ArticleCardRootStyled = styled(ArticleCard.Root)`
      cursor: pointer;
      box-sizing: border-box;
      box-shadow: 0 0 2px 2px ${theme.vars.palette.grey['400']};
      background: #fff;
      position: relative;
      -webkit-transition: 0.3s all ease;
      transition: 0.15s all ease;
      width: 250px;

      &:focus-within {
        box-shadow: 0 0 8px 3px rgba(0, 0, 0, 0.15);
        transform: scale(1.025);
        opacity: 1;
        transition: 0.35s all ease;
      }

      &:hover {
        box-shadow: 0 0 8px 3px rgba(0, 0, 0, 0.15);
        transform: scale(1.025);
        opacity: 1;
        transition: 0.35s all ease;
      }
    `;
    const ArticleCardContentStyled = styled.div`
      display: flex;
      height: 150px;
      overflow: hidden;
      padding: ${theme.vars.spacing.m};
    `;
    const ArticleCardLinkStyled = styled.a`
      color: ${theme.vars.palette.text.primary};
      font-size: ${theme.vars.typography.fontSize3.fontSize};
      text-decoration: none;

      &:hover {
        text-decoration: none;
      }

      &:focus {
        text-decoration: none;
      }
      &::after {
        position: absolute;
        inset: 0;
        display: block;
        content: ' ';
      }
    `;
    const ArticleCardImageStyled = styled(ArticleCard.Image)`
      width: 100%;
    `;
    const ArticleCardImageWrapperStyled = styled.div`
      position: relative;
      height: 120px;
      display: flex;
      justify-content: center;
      align-items: center;
      overflow: hidden;
    `;
    const ArticleCardTitleStyled = styled(ArticleCard.Title)`
      color: ${theme.vars.palette.text.primary};
      font-family: ${theme.vars.typography.fontFamilySystem};
      font-size: ${theme.vars.typography.h3.fontSize};
      font-weight: ${theme.vars.typography.h3.fontWeight};
      line-height: ${theme.vars.typography.h3.lineHeight};
      margin: 0 0 ${theme.vars.spacing.m};
      overflow: hidden;
      display: -webkit-box;
      -webkit-line-clamp: 3; /* number of lines to show */
      -webkit-box-orient: vertical;
    `;
    const ArticleCardSubtitleStyled = styled(ArticleCard.Subtitle)`
      color: ${theme.vars.palette.text.secondary};
      font-family: ${theme.vars.typography.fontFamilySystem};
      font-size: ${theme.vars.typography.fontSize3.fontSize};
      font-weight: ${theme.vars.typography.fontSize3.fontWeight};
      line-height: ${theme.vars.typography.fontSize3.lineHeight};
      margin: 0 0 ${theme.vars.spacing.m};
    `;
    export const ArticleCardStyled = {
      Root: ArticleCardRootStyled,
      Content: ArticleCardContentStyled,
      Link: ArticleCardLinkStyled,
      Image: ArticleCardImageStyled,
      ImageWrapper: ArticleCardImageWrapperStyled,
      Title: ArticleCardTitleStyled,
      Subtitle: ArticleCardSubtitleStyled,
    };

    // misc
    export const TitleStyled = styled.h3`
      color: ${theme.vars.palette.primary.main};
      font-family: ${theme.vars.typography.fontFamilySystem};
      font-size: ${theme.vars.typography.fontSize3.fontSize};
    `;
    export const LoaderContainer = styled.div`
      align-items: center;
      display: flex;
      min-height: 50vh;
    `;
    const Rotate = keyframes`
      from {
        transform: rotate(0deg);
      }
      to {
        transform: rotate(360deg);
      }
    `;
    export const LoaderAnimation = styled.svg`
      animation: ${Rotate} 2s linear infinite;
      display: block;
      fill: ${theme.vars.palette.primary.main};
      height: 50px;
      margin: auto;
      width: 50px;
    `;
    export const GalleryGrid = styled.section`
      display: grid;
      grid-template-columns: auto 1fr auto;
      grid-template-areas:
        'startAction slides endAction'
        'indicators indicators indicators';
      column-gap: 10px;
      position: relative;
    `;
    export const GallerySlides = styled.div`
      grid-area: slides;
    `;
    export const GalleryAction = styled.button`
      align-self: center;
      background-color: ${theme.vars.palette.common.black};
      border: none;
      border-radius: 50%;
      color: ${theme.vars.palette.common.white};
      cursor: pointer;
      margin: 0;
      padding: 0;
      width: 50px;
      height: 50px;

      &[data-direction='right'] {
        grid-area: endAction;
      }

      &[data-direction='left'] {
        grid-area: startAction;
      }

      &:focus,
      &:hover {
        background-color: ${theme.vars.palette.primary.main};
        color: ${theme.vars.palette.primary.contrastText};
      }
    `;
    export const GallerySlideWrapper = styled.div`
      display: none;
      justify-content: center;

      &[data-state='selected'] {
        display: flex;
      }
    `;
    export const GalleryIndicators = styled.div`
      display: flex;
      grid-area: indicators;
      justify-content: center;
      margin-top: ${theme.vars.spacing.m};
      padding-left: 0;
    `;
    export const GalleryIndicator = styled.button`
      border: none;
      position: relative;
      flex: 0 1 auto;
      width: 15px;
      height: 3px;
      margin-right: ${theme.vars.spacing.s};
      margin-left: ${theme.vars.spacing.s};
      text-indent: -999px;
      cursor: pointer;
      background-color: ${theme.vars.palette.common.black};

      &[data-state='active'] {
        background-color: ${theme.vars.palette.primary.main};
      }

      &::before {
        position: absolute;
        top: -10px;
        left: 0;
        display: inline-block;
        width: 100%;
        height: 10px;
        content: '';
      }

      &::after {
        position: absolute;
        bottom: -10px;
        left: 0;
        display: inline-block;
        width: 100%;
        height: 10px;
        content: '';
      }
    `;
    export const RecommendationContainer = styled.div`
      display: inline-block;
      width: auto;
      min-width: 300px;
    `;
    ```

### CSS styled component

CSS styled widget templates use UI primitives which are styled using a `styles.css`. To customize, you can edit the component and CSS.

#### TS CLI { .notintoc }

With the Sitecore Search CLI you can replicate all necessary files of the widget template in your React project. The following procedure includes instructions for installing the CLI in your project and specifying the location where widget components are created.

If you have previously installed the CLI in your project and have a location defined in a file called `.sc-search-settings.json`, you can proceed directly to step 3.

To install the UI Components CLI in your project:

1. To install the Search UI Components CLI, open the terminal of your IDE or command prompt and run the following command:

    ```shell
    npx install --save-dev @sitecore-search/cli
    ```

2. To define the location of where widget components will be created, at the root of your project, in a file called `.sc-search-settings.json`, specify the location as shown in the following code block.

    ```js
    {
        "components-path": "src-test/components"
    }
    ```

3. To create the widget component in `index.tsx` and `styles.css` files, go to the location you defined in the `.sc-search-settings.json` file then open the terminal of your IDE or command prompt and run the following command:

    ```shell
    npx sc-search new-widget --language typescript --template recommendation-gallery --entity content --styling css
    ```

#### TS manual { .notintoc }

When you manually create widget templates in your React project, you need to ensure that you install any required dependencies and create all necessary files.

To create the widget component using this widget template in your project:

1. In your project, to install the development dependency, open the terminal of your IDE or command prompt and run the following command:

    ```shell
    npm install @radix-ui/react-icons@^1.3.0
    ```

2. In a folder for the widget component, create files called `index.tsx` and `styles.css` then in respective files, paste following code blocks.

    ```typescript
    // index.tsx
    import type { KeyboardEvent } from 'react';
    import { useCallback, useEffect, useRef, useState } from 'react';

    import { ArrowLeftIcon, ArrowRightIcon } from '@radix-ui/react-icons';
    import type { RecommendationInitialState } from '@sitecore-search/react';
    import { WidgetDataType, useRecommendation, widget } from '@sitecore-search/react';
    import { ArticleCard, Presence } from '@sitecore-search/ui';

    import './styles.css';

    const KeyboardKeys = {
      RIGHT: 'ArrowRight',
      LEFT: 'ArrowLeft',
    };
    const label = 'Recommendation Gallery';
    const previousLabel = 'Previous';
    const nextLabel = 'Next';
    type ArticleModel = {
      id: string;
      name: string;
      author?: string;
      url?: string;
      image_url?: string;
      source_id?: string;
    };
    type ArticleGalleryProps = {
      autoplay?: boolean;
      autoplaySpeed?: number;
      displayIndicators?: boolean;
      title?: string;
      itemsToDisplay?: number;
    };
    type InitialState = RecommendationInitialState<'itemsPerPage'>;
    const DEFAULT_IMG_URL = 'https://placehold.co/500x300?text=No%20Image'; // TODO: Update with corresponding fallback image

    export const RecommendationGalleryComponent = ({
      autoplay = false,
      autoplaySpeed = 5000,
      displayIndicators = false,
      title = '',
      itemsToDisplay = 6,
    }: ArticleGalleryProps) => {
      const {
        widgetRef,
        actions: { onNavigationNext, onNavigationPrev, onItemClick },
        queryResult: { isLoading, isFetching, data: { content: articles = [] } = {} },
      } = useRecommendation<ArticleModel, InitialState>({
        state: {
          itemsPerPage: itemsToDisplay,
        },
      });
      const totalItems = articles.length;
      const prevActionRef = useRef<HTMLButtonElement>(null);
      const nextActionRef = useRef<HTMLButtonElement>(null);
      const [selectedIndex, setSelectedIndex] = useState(0);
      const goTo = useCallback(
        (index: number) => {
          if (selectedIndex === index) return;
          setSelectedIndex((current) => {
            if (index > current) {
              onNavigationNext({
                index,
              });
            } else {
              onNavigationPrev({
                index,
              });
            }
            return index;
          });
        },
        [selectedIndex, onNavigationNext, onNavigationPrev],
      );
      const handleNext = useCallback(() => {
        setSelectedIndex((current) => {
          const newIndex = current < totalItems - 1 ? current + 1 : 0;
          onNavigationNext({
            index: newIndex,
          });
          return newIndex;
        });
      }, [totalItems, onNavigationNext]);
      const handlePrevious = useCallback(() => {
        setSelectedIndex((current) => {
          const newIndex = current > 0 ? current - 1 : totalItems - 1;
          onNavigationPrev({
            index: newIndex,
          });
          return newIndex;
        });
      }, [totalItems, onNavigationPrev]);
      const handleKeyDown = useCallback(
        (event: KeyboardEvent) => {
          switch (event.key) {
            case KeyboardKeys.RIGHT: {
              event.preventDefault();
              nextActionRef?.current?.focus();
              handleNext();
              break;
            }
            case KeyboardKeys.LEFT: {
              event.preventDefault();
              prevActionRef?.current?.focus();
              handlePrevious();
              break;
            }
            default:
              break;
          }
        },
        [handlePrevious, handleNext],
      );
      useEffect(() => {
        let interval: ReturnType<typeof setInterval>;
        if (autoplay && totalItems) {
          interval = setInterval(handleNext, autoplaySpeed);
        }
        return () => interval && clearInterval(interval);
      }, [totalItems]);
      const loading = isLoading || isFetching;
      return (
        <div className="sitecore-recommendation-container">
          <Presence present={loading}>
            <div className="sitecore-loader-container">
              <svg
                aria-busy={loading}
                aria-hidden={!loading}
                focusable="false"
                role="progressbar"
                viewBox="0 0 20 20"
                className="sitecore-loader-animation"
              >
                <path d="M7.229 1.173a9.25 9.25 0 1 0 11.655 11.412 1.25 1.25 0 1 0-2.4-.698 6.75 6.75 0 1 1-8.506-8.329 1.25 1.25 0 1 0-.75-2.385z" />
              </svg>
            </div>
          </Presence>
          {!loading && articles.length > 0 && (
            <>
              {title && <h3 className="sitecore-title">{title}</h3>}
              <section
                aria-roledescription="carousel"
                aria-label={label}
                onKeyDown={handleKeyDown}
                ref={widgetRef}
                className="sitecore-gallery-grid"
              >
                <button
                  onClick={handlePrevious}
                  data-direction="left"
                  ref={prevActionRef}
                  aria-label={previousLabel}
                  type="button"
                  className="sitecore-gallery-action"
                >
                  <ArrowLeftIcon />
                </button>

                <button
                  onClick={handleNext}
                  data-direction="right"
                  ref={nextActionRef}
                  aria-label={nextLabel}
                  type="button"
                  className="sitecore-gallery-action"
                >
                  <ArrowRightIcon />
                </button>

                <div aria-live={autoplay ? 'off' : 'polite'} className="sitecore-gallery-slides">
                  {articles.map((item, index) => (
                    <div
                      role="group"
                      aria-roledescription="slide"
                      key={item.id}
                      data-state={index === selectedIndex ? 'selected' : ''}
                      aria-label={`${index + 1} of ${totalItems} slides`}
                      className="sitecore-gallery-slide-wrapper"
                    >
                      <ArticleCard.Root key={item.id} className="sitecore-article-card-root">
                        <div className="sitecore-article-card-image-wrapper">
                          <ArticleCard.Image
                            src={item?.image_url || DEFAULT_IMG_URL}
                            className="sitecore-article-card-image"
                          />
                        </div>
                        <div className="sitecore-article-card-content">
                          <a
                            href={item.url}
                            onClick={(event) => {
                              event.preventDefault();
                              onItemClick({
                                id: item.id,
                                index,
                                sourceId: item.source_id,
                              });
                            }}
                            className="sitecore-article-card-link"
                          >
                            <ArticleCard.Title className="sitecore-article-card-title">{item.name}</ArticleCard.Title>
                            <ArticleCard.Subtitle className="sitecore-article-card-subtitle">
                              {item.author}
                            </ArticleCard.Subtitle>
                          </a>
                        </div>
                      </ArticleCard.Root>
                    </div>
                  ))}
                </div>

                {displayIndicators && (
                  <div className="sitecore-gallery-indicators">
                    {articles.map((p, index) => (
                      <button
                        key={p.id}
                        data-slide-to={index}
                        aria-label={`Slide ${index + 1}`}
                        data-state={index === selectedIndex ? 'active' : ''}
                        onClick={() => goTo(index)}
                        type="button"
                        className="sitecore-gallery-indicator"
                      />
                    ))}
                  </div>
                )}
              </section>
            </>
          )}
        </div>
      );
    };
    const RecommendationGalleryWidget = widget(RecommendationGalleryComponent, WidgetDataType.RECOMMENDATION, 'content');
    export default RecommendationGalleryWidget;
    ```

    ```css
    // styles.css
    @keyframes rotate-animation {
      from {
        transform: rotate(0deg);
      }
      to {
        transform: rotate(360deg);
      }
    }

    /* ArticleCardRoot */
    .sitecore-article-card-root {
      cursor: pointer;
      box-sizing: border-box;
      box-shadow: 0 0 2px 2px var(--sdc-palette-grey-400);
      background: #fff;
      position: relative;
      transition: 0.15s all ease;
      width: 250px;
    }

    .sitecore-article-card-root:focus-within {
      box-shadow: 0 0 8px 3px rgb(0 0 0 / 15%);
      transform: scale(1.025);
      opacity: 1;
      transition: 0.35s all ease;
    }

    .sitecore-article-card-root:hover {
      box-shadow: 0 0 8px 3px rgb(0 0 0 / 15%);
      transform: scale(1.025);
      opacity: 1;
      transition: 0.35s all ease;
    }

    /* ArticleCardContent */
    .sitecore-article-card-content {
      display: flex;
      height: 150px;
      overflow: hidden;
      padding: var(--sdc-spacing-m);
    }

    /* ArticleCardLink */
    .sitecore-article-card-link {
      color: var(--sdc-palette-text-primary);
      font-size: var(--sdc-typography-fontSize3-fontSize);
      text-decoration: none;
    }

    .sitecore-article-card-link:hover {
      text-decoration: none;
    }

    .sitecore-article-card-link:focus {
      text-decoration: none;
    }

    .sitecore-article-card-link::after {
      position: absolute;
      inset: 0;
      display: block;
      content: ' ';
    }

    /* ArticleCardImage */
    .sitecore-article-card-image {
      width: 100%;
    }

    /* ArticleCardImageWrapper */
    .sitecore-article-card-image-wrapper {
      position: relative;
      height: 120px;
      display: flex;
      justify-content: center;
      align-items: center;
      overflow: hidden;
    }

    /* ArticleCardTitle */
    .sitecore-article-card-title {
      color: var(--sdc-palette-text-primary);
      font-family: var(--sdc-typography-fontFamilySystem);
      font-size: var(--sdc-typography-h3-fontSize);
      font-weight: var(--sdc-typography-h3-fontWeight);
      line-height: var(--sdc-typography-h3-lineHeight);
      margin: 0 0 var(--sdc-spacing-m);
      overflow: hidden;
      display: -webkit-box;
      -webkit-line-clamp: 3;
      -webkit-box-orient: vertical;
    }

    /* ArticleCardSubtitle */
    .sitecore-article-card-subtitle {
      color: var(--sdc-palette-text-secondary);
      font-family: var(--sdc-typography-fontFamilySystem);
      font-size: var(--sdc-typography-fontSize3-fontSize);
      font-weight: var(--sdc-typography-fontSize3-fontWeight);
      line-height: var(--sdc-typography-fontSize3-lineHeight);
      margin: 0 0 var(--sdc-spacing-m);
    }

    /* Title */
    .sitecore-title {
      color: var(--sdc-palette-primary-main);
      font-family: var(--sdc-typography-fontFamilySystem);
      font-size: var(--sdc-typography-fontSize3-fontSize);
    }

    /* LoaderContainer */
    .sitecore-loader-container {
      align-items: center;
      display: flex;
      min-height: 50vh;
    }

    /* LoaderAnimation */
    .sitecore-loader-animation {
      animation: rotate-animation 2s linear infinite;
      display: block;
      fill: var(--sdc-palette-primary-main);
      height: 50px;
      margin: auto;
      width: 50px;
    }

    /* GalleryGrid */
    .sitecore-gallery-grid {
      display: grid;
      grid-template-columns: auto 1fr auto;
      grid-template-areas:
        'startAction slides endAction'
        'indicators indicators indicators';
      -moz-column-gap: 10px;
      column-gap: 10px;
      position: relative;
    }

    /* GallerySlides */
    .sitecore-gallery-slides {
      grid-area: slides;
    }

    /* GalleryAction */
    .sitecore-gallery-action {
      align-self: center;
      background-color: var(--sdc-palette-common-black);
      border: none;
      border-radius: 50%;
      color: var(--sdc-palette-common-white);
      cursor: pointer;
      margin: 0;
      padding: 0;
      width: 50px;
      height: 50px;
    }

    .sitecore-gallery-action[data-direction='right'] {
      grid-area: endAction;
    }

    .sitecore-gallery-action[data-direction='left'] {
      grid-area: startAction;
    }

    .sitecore-gallery-action:focus,
    .sitecore-gallery-action:hover {
      background-color: var(--sdc-palette-primary-main);
      color: var(--sdc-palette-primary-contrastText);
    }

    /* GallerySlideWrapper */
    .sitecore-gallery-slide-wrapper {
      display: none;
      justify-content: center;
    }

    .sitecore-gallery-slide-wrapper[data-state='selected'] {
      display: flex;
    }

    /* GalleryIndicators */
    .sitecore-gallery-indicators {
      display: flex;
      grid-area: indicators;
      justify-content: center;
      margin-top: var(--sdc-spacing-m);
      padding-left: 0;
    }

    /* GalleryIndicator */
    .sitecore-gallery-indicator {
      border: none;
      position: relative;
      flex: 0 1 auto;
      width: 15px;
      height: 3px;
      margin-right: var(--sdc-spacing-s);
      margin-left: var(--sdc-spacing-s);
      text-indent: -999px;
      cursor: pointer;
      background-color: var(--sdc-palette-common-black);
    }

    .sitecore-gallery-indicator[data-state='active'] {
      background-color: var(--sdc-palette-primary-main);
    }

    .sitecore-gallery-indicator::before {
      position: absolute;
      top: -10px;
      left: 0;
      display: inline-block;
      width: 100%;
      height: 10px;
      content: '';
    }

    .sitecore-gallery-indicator::after {
      position: absolute;
      bottom: -10px;
      left: 0;
      display: inline-block;
      width: 100%;
      height: 10px;
      content: '';
    }

    /* RecommendationContainer */
    .sitecore-recommendation-container {
      display: inline-block;
      width: auto;
      min-width: 300px;
    }
    ```

#### JS CLI { .notintoc }

With the Sitecore Search CLI you can replicate all necessary files of the widget template in your React project. The following procedure includes instructions for installing the CLI in your project and specifying the location where widget components are created.

If you have previously installed the CLI in your project and have a location defined in a file called `.sc-search-settings.json`, you can proceed directly to step 3.

To install the UI Components CLI in your project:

1. To install the Search UI Components CLI, open the terminal of your IDE or command prompt and run the following command:

    ```shell
    npx install --save-dev @sitecore-search/cli
    ```

2. To define the location of where widget components will be created, at the root of your project, in a file called `.sc-search-settings.json`, specify the location as shown in the following code block.

    ```js
    {
        "components-path": "src-test/components"
    }
    ```

3. To create the widget component in `index.jsx` and `styles.css` files, go to the location you defined in the `.sc-search-settings.json` file then open the terminal of your IDE or command prompt and run the following command:

    ```shell
    npx sc-search new-widget --language javascript --template recommendation-gallery --entity content --styling css
    ```

#### JS manual { .notintoc }

When you manually create widget templates in your React project, you need to ensure that you install any required dependencies and create all necessary files.

To create the widget component using this widget template in your project:

1. In your project, to install the development dependency, open the terminal of your IDE or command prompt and run the following command:

    ```shell
    npm install @radix-ui/react-icons@^1.3.0
    ```

2. In a folder for the widget component, create files called `index.jsx` and `styles.css` then in respective files, paste following code blocks.

    ```javascript
    // index.jsx
    import { useCallback, useEffect, useRef, useState } from 'react';

    import { ArrowLeftIcon, ArrowRightIcon } from '@radix-ui/react-icons';
    import { WidgetDataType, useRecommendation, widget } from '@sitecore-search/react';
    import { ArticleCard, Presence } from '@sitecore-search/ui';

    import './styles.css';

    const KeyboardKeys = {
      RIGHT: 'ArrowRight',
      LEFT: 'ArrowLeft',
    };
    const label = 'Recommendation Gallery';
    const previousLabel = 'Previous';
    const nextLabel = 'Next';
    const DEFAULT_IMG_URL = 'https://placehold.co/500x300?text=No%20Image'; // TODO: Update with corresponding fallback image

    export const RecommendationGalleryComponent = ({
      autoplay = false,
      autoplaySpeed = 5000,
      displayIndicators = false,
      title = '',
      itemsToDisplay = 6,
    }) => {
      const {
        widgetRef,
        actions: { onNavigationNext, onNavigationPrev, onItemClick },
        queryResult: { isLoading, isFetching, data: { content: articles = [] } = {} },
      } = useRecommendation({
        state: {
          itemsPerPage: itemsToDisplay,
        },
      });
      const totalItems = articles.length;
      const prevActionRef = useRef(null);
      const nextActionRef = useRef(null);
      const [selectedIndex, setSelectedIndex] = useState(0);
      const goTo = useCallback(
        (index) => {
          if (selectedIndex === index) return;
          setSelectedIndex((current) => {
            if (index > current) {
              onNavigationNext({
                index,
              });
            } else {
              onNavigationPrev({
                index,
              });
            }
            return index;
          });
        },
        [selectedIndex, onNavigationNext, onNavigationPrev],
      );
      const handleNext = useCallback(() => {
        setSelectedIndex((current) => {
          const newIndex = current < totalItems - 1 ? current + 1 : 0;
          onNavigationNext({
            index: newIndex,
          });
          return newIndex;
        });
      }, [totalItems, onNavigationNext]);
      const handlePrevious = useCallback(() => {
        setSelectedIndex((current) => {
          const newIndex = current > 0 ? current - 1 : totalItems - 1;
          onNavigationPrev({
            index: newIndex,
          });
          return newIndex;
        });
      }, [totalItems, onNavigationPrev]);
      const handleKeyDown = useCallback(
        (event) => {
          switch (event.key) {
            case KeyboardKeys.RIGHT: {
              event.preventDefault();
              nextActionRef?.current?.focus();
              handleNext();
              break;
            }
            case KeyboardKeys.LEFT: {
              event.preventDefault();
              prevActionRef?.current?.focus();
              handlePrevious();
              break;
            }
            default:
              break;
          }
        },
        [handlePrevious, handleNext],
      );
      useEffect(() => {
        let interval;
        if (autoplay && totalItems) {
          interval = setInterval(handleNext, autoplaySpeed);
        }
        return () => interval && clearInterval(interval);
      }, [totalItems]);
      const loading = isLoading || isFetching;
      return (
        <div className="sitecore-recommendation-container">
          <Presence present={loading}>
            <div className="sitecore-loader-container">
              <svg
                aria-busy={loading}
                aria-hidden={!loading}
                focusable="false"
                role="progressbar"
                viewBox="0 0 20 20"
                className="sitecore-loader-animation"
              >
                <path d="M7.229 1.173a9.25 9.25 0 1 0 11.655 11.412 1.25 1.25 0 1 0-2.4-.698 6.75 6.75 0 1 1-8.506-8.329 1.25 1.25 0 1 0-.75-2.385z" />
              </svg>
            </div>
          </Presence>
          {!loading && articles.length > 0 && (
            <>
              {title && <h3 className="sitecore-title">{title}</h3>}
              <section
                aria-roledescription="carousel"
                aria-label={label}
                onKeyDown={handleKeyDown}
                ref={widgetRef}
                className="sitecore-gallery-grid"
              >
                <button
                  onClick={handlePrevious}
                  data-direction="left"
                  ref={prevActionRef}
                  aria-label={previousLabel}
                  type="button"
                  className="sitecore-gallery-action"
                >
                  <ArrowLeftIcon />
                </button>

                <button
                  onClick={handleNext}
                  data-direction="right"
                  ref={nextActionRef}
                  aria-label={nextLabel}
                  type="button"
                  className="sitecore-gallery-action"
                >
                  <ArrowRightIcon />
                </button>

                <div aria-live={autoplay ? 'off' : 'polite'} className="sitecore-gallery-slides">
                  {articles.map((item, index) => (
                    <div
                      role="group"
                      aria-roledescription="slide"
                      key={item.id}
                      data-state={index === selectedIndex ? 'selected' : ''}
                      aria-label={`${index + 1} of ${totalItems} slides`}
                      className="sitecore-gallery-slide-wrapper"
                    >
                      <ArticleCard.Root key={item.id} className="sitecore-article-card-root">
                        <div className="sitecore-article-card-image-wrapper">
                          <ArticleCard.Image
                            src={item?.image_url || DEFAULT_IMG_URL}
                            className="sitecore-article-card-image"
                          />
                        </div>
                        <div className="sitecore-article-card-content">
                          <a
                            href={item.url}
                            onClick={(event) => {
                              event.preventDefault();
                              onItemClick({
                                id: item.id,
                                index,
                                sourceId: item.source_id,
                              });
                            }}
                            className="sitecore-article-card-link"
                          >
                            <ArticleCard.Title className="sitecore-article-card-title">{item.name}</ArticleCard.Title>
                            <ArticleCard.Subtitle className="sitecore-article-card-subtitle">
                              {item.author}
                            </ArticleCard.Subtitle>
                          </a>
                        </div>
                      </ArticleCard.Root>
                    </div>
                  ))}
                </div>

                {displayIndicators && (
                  <div className="sitecore-gallery-indicators">
                    {articles.map((p, index) => (
                      <button
                        key={p.id}
                        data-slide-to={index}
                        aria-label={`Slide ${index + 1}`}
                        data-state={index === selectedIndex ? 'active' : ''}
                        onClick={() => goTo(index)}
                        type="button"
                        className="sitecore-gallery-indicator"
                      />
                    ))}
                  </div>
                )}
              </section>
            </>
          )}
        </div>
      );
    };
    const RecommendationGalleryWidget = widget(RecommendationGalleryComponent, WidgetDataType.RECOMMENDATION, 'content');
    export default RecommendationGalleryWidget;
    ```

    ```css
    // styles.css
    @keyframes rotate-animation {
      from {
        transform: rotate(0deg);
      }
      to {
        transform: rotate(360deg);
      }
    }

    /* ArticleCardRoot */
    .sitecore-article-card-root {
      cursor: pointer;
      box-sizing: border-box;
      box-shadow: 0 0 2px 2px var(--sdc-palette-grey-400);
      background: #fff;
      position: relative;
      transition: 0.15s all ease;
      width: 250px;
    }

    .sitecore-article-card-root:focus-within {
      box-shadow: 0 0 8px 3px rgb(0 0 0 / 15%);
      transform: scale(1.025);
      opacity: 1;
      transition: 0.35s all ease;
    }

    .sitecore-article-card-root:hover {
      box-shadow: 0 0 8px 3px rgb(0 0 0 / 15%);
      transform: scale(1.025);
      opacity: 1;
      transition: 0.35s all ease;
    }

    /* ArticleCardContent */
    .sitecore-article-card-content {
      display: flex;
      height: 150px;
      overflow: hidden;
      padding: var(--sdc-spacing-m);
    }

    /* ArticleCardLink */
    .sitecore-article-card-link {
      color: var(--sdc-palette-text-primary);
      font-size: var(--sdc-typography-fontSize3-fontSize);
      text-decoration: none;
    }

    .sitecore-article-card-link:hover {
      text-decoration: none;
    }

    .sitecore-article-card-link:focus {
      text-decoration: none;
    }

    .sitecore-article-card-link::after {
      position: absolute;
      inset: 0;
      display: block;
      content: ' ';
    }

    /* ArticleCardImage */
    .sitecore-article-card-image {
      width: 100%;
    }

    /* ArticleCardImageWrapper */
    .sitecore-article-card-image-wrapper {
      position: relative;
      height: 120px;
      display: flex;
      justify-content: center;
      align-items: center;
      overflow: hidden;
    }

    /* ArticleCardTitle */
    .sitecore-article-card-title {
      color: var(--sdc-palette-text-primary);
      font-family: var(--sdc-typography-fontFamilySystem);
      font-size: var(--sdc-typography-h3-fontSize);
      font-weight: var(--sdc-typography-h3-fontWeight);
      line-height: var(--sdc-typography-h3-lineHeight);
      margin: 0 0 var(--sdc-spacing-m);
      overflow: hidden;
      display: -webkit-box;
      -webkit-line-clamp: 3;
      -webkit-box-orient: vertical;
    }

    /* ArticleCardSubtitle */
    .sitecore-article-card-subtitle {
      color: var(--sdc-palette-text-secondary);
      font-family: var(--sdc-typography-fontFamilySystem);
      font-size: var(--sdc-typography-fontSize3-fontSize);
      font-weight: var(--sdc-typography-fontSize3-fontWeight);
      line-height: var(--sdc-typography-fontSize3-lineHeight);
      margin: 0 0 var(--sdc-spacing-m);
    }

    /* Title */
    .sitecore-title {
      color: var(--sdc-palette-primary-main);
      font-family: var(--sdc-typography-fontFamilySystem);
      font-size: var(--sdc-typography-fontSize3-fontSize);
    }

    /* LoaderContainer */
    .sitecore-loader-container {
      align-items: center;
      display: flex;
      min-height: 50vh;
    }

    /* LoaderAnimation */
    .sitecore-loader-animation {
      animation: rotate-animation 2s linear infinite;
      display: block;
      fill: var(--sdc-palette-primary-main);
      height: 50px;
      margin: auto;
      width: 50px;
    }

    /* GalleryGrid */
    .sitecore-gallery-grid {
      display: grid;
      grid-template-columns: auto 1fr auto;
      grid-template-areas:
        'startAction slides endAction'
        'indicators indicators indicators';
      -moz-column-gap: 10px;
      column-gap: 10px;
      position: relative;
    }

    /* GallerySlides */
    .sitecore-gallery-slides {
      grid-area: slides;
    }

    /* GalleryAction */
    .sitecore-gallery-action {
      align-self: center;
      background-color: var(--sdc-palette-common-black);
      border: none;
      border-radius: 50%;
      color: var(--sdc-palette-common-white);
      cursor: pointer;
      margin: 0;
      padding: 0;
      width: 50px;
      height: 50px;
    }

    .sitecore-gallery-action[data-direction='right'] {
      grid-area: endAction;
    }

    .sitecore-gallery-action[data-direction='left'] {
      grid-area: startAction;
    }

    .sitecore-gallery-action:focus,
    .sitecore-gallery-action:hover {
      background-color: var(--sdc-palette-primary-main);
      color: var(--sdc-palette-primary-contrastText);
    }

    /* GallerySlideWrapper */
    .sitecore-gallery-slide-wrapper {
      display: none;
      justify-content: center;
    }

    .sitecore-gallery-slide-wrapper[data-state='selected'] {
      display: flex;
    }

    /* GalleryIndicators */
    .sitecore-gallery-indicators {
      display: flex;
      grid-area: indicators;
      justify-content: center;
      margin-top: var(--sdc-spacing-m);
      padding-left: 0;
    }

    /* GalleryIndicator */
    .sitecore-gallery-indicator {
      border: none;
      position: relative;
      flex: 0 1 auto;
      width: 15px;
      height: 3px;
      margin-right: var(--sdc-spacing-s);
      margin-left: var(--sdc-spacing-s);
      text-indent: -999px;
      cursor: pointer;
      background-color: var(--sdc-palette-common-black);
    }

    .sitecore-gallery-indicator[data-state='active'] {
      background-color: var(--sdc-palette-primary-main);
    }

    .sitecore-gallery-indicator::before {
      position: absolute;
      top: -10px;
      left: 0;
      display: inline-block;
      width: 100%;
      height: 10px;
      content: '';
    }

    .sitecore-gallery-indicator::after {
      position: absolute;
      bottom: -10px;
      left: 0;
      display: inline-block;
      width: 100%;
      height: 10px;
      content: '';
    }

    /* RecommendationContainer */
    .sitecore-recommendation-container {
      display: inline-block;
      width: auto;
      min-width: 300px;
    }
    ```

### CSS module styled component

CSS module styled widget templates use UI primitives which are styled using a CSS module. To customize, you can edit the component and CSS module.

#### TS CLI { .notintoc }

With the Sitecore Search CLI you can replicate all necessary files of the widget template in your React project. The following procedure includes instructions for installing the CLI in your project and specifying the location where widget components are created.

If you have previously installed the CLI in your project and have a location defined in a file called `.sc-search-settings.json`, you can proceed directly to step 3.

To install the UI Components CLI in your project:

1. To install the Search UI Components CLI, open the terminal of your IDE or command prompt and run the following command:

    ```shell
    npx install --save-dev @sitecore-search/cli
    ```

2. To define the location of where widget components will be created, at the root of your project, in a file called `.sc-search-settings.json`, specify the location as shown in the following code block.

    ```js
    {
        "components-path": "src-test/components"
    }
    ```

3. To create the widget component in `index.tsx` and `styles.module.css` files, go to the location you defined in the `.sc-search-settings.json` file then open the terminal of your IDE or command prompt and run the following command:

    ```shell
    npx sc-search new-widget --language typescript --template recommendation-gallery --entity content --styling cssModule
    ```

#### TS manual { .notintoc }

When you manually create widget templates in your React project, you need to ensure that you install any required dependencies and create all necessary files.

To create the widget component using this widget template in your project:

1. In your project, to install the development dependency, open the terminal of your IDE or command prompt and run the following command:

    ```shell
    npm install @radix-ui/react-icons@^1.3.0
    ```

2. In a folder for the widget component, create files called `index.tsx` and `styles.module.css` then in respective files, paste following code blocks.

    ```typescript
    // index.tsx
    import type { KeyboardEvent } from 'react';
    import { useCallback, useEffect, useRef, useState } from 'react';

    import { ArrowLeftIcon, ArrowRightIcon } from '@radix-ui/react-icons';
    import type { RecommendationInitialState } from '@sitecore-search/react';
    import { WidgetDataType, useRecommendation, widget } from '@sitecore-search/react';
    import { ArticleCard, Presence } from '@sitecore-search/ui';

    import styles from './styles.module.css';

    const KeyboardKeys = {
      RIGHT: 'ArrowRight',
      LEFT: 'ArrowLeft',
    };
    const label = 'Recommendation Gallery';
    const previousLabel = 'Previous';
    const nextLabel = 'Next';
    type ArticleModel = {
      id: string;
      name: string;
      author?: string;
      url?: string;
      image_url?: string;
      source_id?: string;
    };
    type ArticleGalleryProps = {
      autoplay?: boolean;
      autoplaySpeed?: number;
      displayIndicators?: boolean;
      title?: string;
      itemsToDisplay?: number;
    };
    type InitialState = RecommendationInitialState<'itemsPerPage'>;
    const DEFAULT_IMG_URL = 'https://placehold.co/500x300?text=No%20Image'; // TODO: Update with corresponding fallback image

    export const RecommendationGalleryComponent = ({
      autoplay = false,
      autoplaySpeed = 5000,
      displayIndicators = false,
      title = '',
      itemsToDisplay = 6,
    }: ArticleGalleryProps) => {
      const {
        widgetRef,
        actions: { onNavigationNext, onNavigationPrev, onItemClick },
        queryResult: { isLoading, isFetching, data: { content: articles = [] } = {} },
      } = useRecommendation<ArticleModel, InitialState>({
        state: {
          itemsPerPage: itemsToDisplay,
        },
      });
      const totalItems = articles.length;
      const prevActionRef = useRef<HTMLButtonElement>(null);
      const nextActionRef = useRef<HTMLButtonElement>(null);
      const [selectedIndex, setSelectedIndex] = useState(0);
      const goTo = useCallback(
        (index: number) => {
          if (selectedIndex === index) return;
          setSelectedIndex((current) => {
            if (index > current) {
              onNavigationNext({
                index,
              });
            } else {
              onNavigationPrev({
                index,
              });
            }
            return index;
          });
        },
        [selectedIndex, onNavigationNext, onNavigationPrev],
      );
      const handleNext = useCallback(() => {
        setSelectedIndex((current) => {
          const newIndex = current < totalItems - 1 ? current + 1 : 0;
          onNavigationNext({
            index: newIndex,
          });
          return newIndex;
        });
      }, [totalItems, onNavigationNext]);
      const handlePrevious = useCallback(() => {
        setSelectedIndex((current) => {
          const newIndex = current > 0 ? current - 1 : totalItems - 1;
          onNavigationPrev({
            index: newIndex,
          });
          return newIndex;
        });
      }, [totalItems, onNavigationPrev]);
      const handleKeyDown = useCallback(
        (event: KeyboardEvent) => {
          switch (event.key) {
            case KeyboardKeys.RIGHT: {
              event.preventDefault();
              nextActionRef?.current?.focus();
              handleNext();
              break;
            }
            case KeyboardKeys.LEFT: {
              event.preventDefault();
              prevActionRef?.current?.focus();
              handlePrevious();
              break;
            }
            default:
              break;
          }
        },
        [handlePrevious, handleNext],
      );
      useEffect(() => {
        let interval: ReturnType<typeof setInterval>;
        if (autoplay && totalItems) {
          interval = setInterval(handleNext, autoplaySpeed);
        }
        return () => interval && clearInterval(interval);
      }, [totalItems]);
      const loading = isLoading || isFetching;
      return (
        <div className={styles['sitecore-recommendation-container']}>
          <Presence present={loading}>
            <div className={styles['sitecore-loader-container']}>
              <svg
                aria-busy={loading}
                aria-hidden={!loading}
                focusable="false"
                role="progressbar"
                viewBox="0 0 20 20"
                className={styles['sitecore-loader-animation']}
              >
                <path d="M7.229 1.173a9.25 9.25 0 1 0 11.655 11.412 1.25 1.25 0 1 0-2.4-.698 6.75 6.75 0 1 1-8.506-8.329 1.25 1.25 0 1 0-.75-2.385z" />
              </svg>
            </div>
          </Presence>
          {!loading && articles.length > 0 && (
            <>
              {title && <h3 className={styles['sitecore-title']}>{title}</h3>}
              <section
                aria-roledescription="carousel"
                aria-label={label}
                onKeyDown={handleKeyDown}
                ref={widgetRef}
                className={styles['sitecore-gallery-grid']}
              >
                <button
                  onClick={handlePrevious}
                  data-direction="left"
                  ref={prevActionRef}
                  aria-label={previousLabel}
                  type="button"
                  className={styles['sitecore-gallery-action']}
                >
                  <ArrowLeftIcon />
                </button>

                <button
                  onClick={handleNext}
                  data-direction="right"
                  ref={nextActionRef}
                  aria-label={nextLabel}
                  type="button"
                  className={styles['sitecore-gallery-action']}
                >
                  <ArrowRightIcon />
                </button>

                <div aria-live={autoplay ? 'off' : 'polite'} className={styles['sitecore-gallery-slides']}>
                  {articles.map((item, index) => (
                    <div
                      role="group"
                      aria-roledescription="slide"
                      key={item.id}
                      data-state={index === selectedIndex ? 'selected' : ''}
                      aria-label={`${index + 1} of ${totalItems} slides`}
                      className={styles['sitecore-gallery-slide-wrapper']}
                    >
                      <ArticleCard.Root key={item.id} className={styles['sitecore-article-card-root']}>
                        <div className={styles['sitecore-article-card-image-wrapper']}>
                          <ArticleCard.Image
                            src={item?.image_url || DEFAULT_IMG_URL}
                            className={styles['sitecore-article-card-image']}
                          />
                        </div>
                        <div className={styles['sitecore-article-card-content']}>
                          <a
                            href={item.url}
                            onClick={(event) => {
                              event.preventDefault();
                              onItemClick({
                                id: item.id,
                                index,
                                sourceId: item.source_id,
                              });
                            }}
                            className={styles['sitecore-article-card-link']}
                          >
                            <ArticleCard.Title className={styles['sitecore-article-card-title']}>
                              {item.name}
                            </ArticleCard.Title>
                            <ArticleCard.Subtitle className={styles['sitecore-article-card-subtitle']}>
                              {item.author}
                            </ArticleCard.Subtitle>
                          </a>
                        </div>
                      </ArticleCard.Root>
                    </div>
                  ))}
                </div>

                {displayIndicators && (
                  <div className={styles['sitecore-gallery-indicators']}>
                    {articles.map((p, index) => (
                      <button
                        key={p.id}
                        data-slide-to={index}
                        aria-label={`Slide ${index + 1}`}
                        data-state={index === selectedIndex ? 'active' : ''}
                        onClick={() => goTo(index)}
                        type="button"
                        className={styles['sitecore-gallery-indicator']}
                      />
                    ))}
                  </div>
                )}
              </section>
            </>
          )}
        </div>
      );
    };
    const RecommendationGalleryWidget = widget(RecommendationGalleryComponent, WidgetDataType.RECOMMENDATION, 'content');
    export default RecommendationGalleryWidget;
    ```

    ```css
    // styles.module.css
    @keyframes rotate-animation {
      from {
        transform: rotate(0deg);
      }
      to {
        transform: rotate(360deg);
      }
    }

    /* ArticleCardRoot */
    .sitecore-article-card-root {
      cursor: pointer;
      box-sizing: border-box;
      box-shadow: 0 0 2px 2px var(--sdc-palette-grey-400);
      background: #fff;
      position: relative;
      transition: 0.15s all ease;
      width: 250px;
    }

    .sitecore-article-card-root:focus-within {
      box-shadow: 0 0 8px 3px rgb(0 0 0 / 15%);
      transform: scale(1.025);
      opacity: 1;
      transition: 0.35s all ease;
    }

    .sitecore-article-card-root:hover {
      box-shadow: 0 0 8px 3px rgb(0 0 0 / 15%);
      transform: scale(1.025);
      opacity: 1;
      transition: 0.35s all ease;
    }

    /* ArticleCardContent */
    .sitecore-article-card-content {
      display: flex;
      height: 150px;
      overflow: hidden;
      padding: var(--sdc-spacing-m);
    }

    /* ArticleCardLink */
    .sitecore-article-card-link {
      color: var(--sdc-palette-text-primary);
      font-size: var(--sdc-typography-fontSize3-fontSize);
      text-decoration: none;
    }

    .sitecore-article-card-link:hover {
      text-decoration: none;
    }

    .sitecore-article-card-link:focus {
      text-decoration: none;
    }

    .sitecore-article-card-link::after {
      position: absolute;
      inset: 0;
      display: block;
      content: ' ';
    }

    /* ArticleCardImage */
    .sitecore-article-card-image {
      width: 100%;
    }

    /* ArticleCardImageWrapper */
    .sitecore-article-card-image-wrapper {
      position: relative;
      height: 120px;
      display: flex;
      justify-content: center;
      align-items: center;
      overflow: hidden;
    }

    /* ArticleCardTitle */
    .sitecore-article-card-title {
      color: var(--sdc-palette-text-primary);
      font-family: var(--sdc-typography-fontFamilySystem);
      font-size: var(--sdc-typography-h3-fontSize);
      font-weight: var(--sdc-typography-h3-fontWeight);
      line-height: var(--sdc-typography-h3-lineHeight);
      margin: 0 0 var(--sdc-spacing-m);
      overflow: hidden;
      display: -webkit-box;
      -webkit-line-clamp: 3;
      -webkit-box-orient: vertical;
    }

    /* ArticleCardSubtitle */
    .sitecore-article-card-subtitle {
      color: var(--sdc-palette-text-secondary);
      font-family: var(--sdc-typography-fontFamilySystem);
      font-size: var(--sdc-typography-fontSize3-fontSize);
      font-weight: var(--sdc-typography-fontSize3-fontWeight);
      line-height: var(--sdc-typography-fontSize3-lineHeight);
      margin: 0 0 var(--sdc-spacing-m);
    }

    /* Title */
    .sitecore-title {
      color: var(--sdc-palette-primary-main);
      font-family: var(--sdc-typography-fontFamilySystem);
      font-size: var(--sdc-typography-fontSize3-fontSize);
    }

    /* LoaderContainer */
    .sitecore-loader-container {
      align-items: center;
      display: flex;
      min-height: 50vh;
    }

    /* LoaderAnimation */
    .sitecore-loader-animation {
      animation: rotate-animation 2s linear infinite;
      display: block;
      fill: var(--sdc-palette-primary-main);
      height: 50px;
      margin: auto;
      width: 50px;
    }

    /* GalleryGrid */
    .sitecore-gallery-grid {
      display: grid;
      grid-template-columns: auto 1fr auto;
      grid-template-areas:
        'startAction slides endAction'
        'indicators indicators indicators';
      -moz-column-gap: 10px;
      column-gap: 10px;
      position: relative;
    }

    /* GallerySlides */
    .sitecore-gallery-slides {
      grid-area: slides;
    }

    /* GalleryAction */
    .sitecore-gallery-action {
      align-self: center;
      background-color: var(--sdc-palette-common-black);
      border: none;
      border-radius: 50%;
      color: var(--sdc-palette-common-white);
      cursor: pointer;
      margin: 0;
      padding: 0;
      width: 50px;
      height: 50px;
    }

    .sitecore-gallery-action[data-direction='right'] {
      grid-area: endAction;
    }

    .sitecore-gallery-action[data-direction='left'] {
      grid-area: startAction;
    }

    .sitecore-gallery-action:focus,
    .sitecore-gallery-action:hover {
      background-color: var(--sdc-palette-primary-main);
      color: var(--sdc-palette-primary-contrastText);
    }

    /* GallerySlideWrapper */
    .sitecore-gallery-slide-wrapper {
      display: none;
      justify-content: center;
    }

    .sitecore-gallery-slide-wrapper[data-state='selected'] {
      display: flex;
    }

    /* GalleryIndicators */
    .sitecore-gallery-indicators {
      display: flex;
      grid-area: indicators;
      justify-content: center;
      margin-top: var(--sdc-spacing-m);
      padding-left: 0;
    }

    /* GalleryIndicator */
    .sitecore-gallery-indicator {
      border: none;
      position: relative;
      flex: 0 1 auto;
      width: 15px;
      height: 3px;
      margin-right: var(--sdc-spacing-s);
      margin-left: var(--sdc-spacing-s);
      text-indent: -999px;
      cursor: pointer;
      background-color: var(--sdc-palette-common-black);
    }

    .sitecore-gallery-indicator[data-state='active'] {
      background-color: var(--sdc-palette-primary-main);
    }

    .sitecore-gallery-indicator::before {
      position: absolute;
      top: -10px;
      left: 0;
      display: inline-block;
      width: 100%;
      height: 10px;
      content: '';
    }

    .sitecore-gallery-indicator::after {
      position: absolute;
      bottom: -10px;
      left: 0;
      display: inline-block;
      width: 100%;
      height: 10px;
      content: '';
    }

    /* RecommendationContainer */
    .sitecore-recommendation-container {
      display: inline-block;
      width: auto;
      min-width: 300px;
    }
    ```

#### JS CLI { .notintoc }

With the Sitecore Search CLI you can replicate all necessary files of the widget template in your React project. The following procedure includes instructions for installing the CLI in your project and specifying the location where widget components are created.

If you have previously installed the CLI in your project and have a location defined in a file called `.sc-search-settings.json`, you can proceed directly to step 3.

To install the UI Components CLI in your project:

1. To install the Search UI Components CLI, open the terminal of your IDE or command prompt and run the following command:

    ```shell
    npx install --save-dev @sitecore-search/cli
    ```

2. To define the location of where widget components will be created, at the root of your project, in a file called `.sc-search-settings.json`, specify the location as shown in the following code block.

    ```js
    {
        "components-path": "src-test/components"
    }
    ```

3. To create the widget component in `index.jsx` and `styles.module.css` files, go to the location you defined in the `.sc-search-settings.json` file then open the terminal of your IDE or command prompt and run the following command:

    ```shell
    npx sc-search new-widget --language javascript --template recommendation-gallery --entity content --styling cssModulenpx sc-search new-widget --language javascript --template recommendation-gallery --entity content --styling cssModule
    ```

#### JS manual { .notintoc }

When you manually create widget templates in your React project, you need to ensure that you install any required dependencies and create all necessary files.

To create the widget component using this widget template in your project:

1. In your project, to install the development dependency, open the terminal of your IDE or command prompt and run the following command:

    ```shell
    npm install @radix-ui/react-icons@^1.3.0
    ```

2. In a folder for the widget component, create files called `index.jsx` and `styles.module.css` then in respective files, paste following code blocks.

    ```javascript
    // index.jsx
    import { useCallback, useEffect, useRef, useState } from 'react';

    import { ArrowLeftIcon, ArrowRightIcon } from '@radix-ui/react-icons';
    import { WidgetDataType, useRecommendation, widget } from '@sitecore-search/react';
    import { ArticleCard, Presence } from '@sitecore-search/ui';

    import styles from './styles.module.css';

    const KeyboardKeys = {
      RIGHT: 'ArrowRight',
      LEFT: 'ArrowLeft',
    };
    const label = 'Recommendation Gallery';
    const previousLabel = 'Previous';
    const nextLabel = 'Next';
    const DEFAULT_IMG_URL = 'https://placehold.co/500x300?text=No%20Image'; // TODO: Update with corresponding fallback image

    export const RecommendationGalleryComponent = ({
      autoplay = false,
      autoplaySpeed = 5000,
      displayIndicators = false,
      title = '',
      itemsToDisplay = 6,
    }) => {
      const {
        widgetRef,
        actions: { onNavigationNext, onNavigationPrev, onItemClick },
        queryResult: { isLoading, isFetching, data: { content: articles = [] } = {} },
      } = useRecommendation({
        state: {
          itemsPerPage: itemsToDisplay,
        },
      });
      const totalItems = articles.length;
      const prevActionRef = useRef(null);
      const nextActionRef = useRef(null);
      const [selectedIndex, setSelectedIndex] = useState(0);
      const goTo = useCallback(
        (index) => {
          if (selectedIndex === index) return;
          setSelectedIndex((current) => {
            if (index > current) {
              onNavigationNext({
                index,
              });
            } else {
              onNavigationPrev({
                index,
              });
            }
            return index;
          });
        },
        [selectedIndex, onNavigationNext, onNavigationPrev],
      );
      const handleNext = useCallback(() => {
        setSelectedIndex((current) => {
          const newIndex = current < totalItems - 1 ? current + 1 : 0;
          onNavigationNext({
            index: newIndex,
          });
          return newIndex;
        });
      }, [totalItems, onNavigationNext]);
      const handlePrevious = useCallback(() => {
        setSelectedIndex((current) => {
          const newIndex = current > 0 ? current - 1 : totalItems - 1;
          onNavigationPrev({
            index: newIndex,
          });
          return newIndex;
        });
      }, [totalItems, onNavigationPrev]);
      const handleKeyDown = useCallback(
        (event) => {
          switch (event.key) {
            case KeyboardKeys.RIGHT: {
              event.preventDefault();
              nextActionRef?.current?.focus();
              handleNext();
              break;
            }
            case KeyboardKeys.LEFT: {
              event.preventDefault();
              prevActionRef?.current?.focus();
              handlePrevious();
              break;
            }
            default:
              break;
          }
        },
        [handlePrevious, handleNext],
      );
      useEffect(() => {
        let interval;
        if (autoplay && totalItems) {
          interval = setInterval(handleNext, autoplaySpeed);
        }
        return () => interval && clearInterval(interval);
      }, [totalItems]);
      const loading = isLoading || isFetching;
      return (
        <div className={styles['sitecore-recommendation-container']}>
          <Presence present={loading}>
            <div className={styles['sitecore-loader-container']}>
              <svg
                aria-busy={loading}
                aria-hidden={!loading}
                focusable="false"
                role="progressbar"
                viewBox="0 0 20 20"
                className={styles['sitecore-loader-animation']}
              >
                <path d="M7.229 1.173a9.25 9.25 0 1 0 11.655 11.412 1.25 1.25 0 1 0-2.4-.698 6.75 6.75 0 1 1-8.506-8.329 1.25 1.25 0 1 0-.75-2.385z" />
              </svg>
            </div>
          </Presence>
          {!loading && articles.length > 0 && (
            <>
              {title && <h3 className={styles['sitecore-title']}>{title}</h3>}
              <section
                aria-roledescription="carousel"
                aria-label={label}
                onKeyDown={handleKeyDown}
                ref={widgetRef}
                className={styles['sitecore-gallery-grid']}
              >
                <button
                  onClick={handlePrevious}
                  data-direction="left"
                  ref={prevActionRef}
                  aria-label={previousLabel}
                  type="button"
                  className={styles['sitecore-gallery-action']}
                >
                  <ArrowLeftIcon />
                </button>

                <button
                  onClick={handleNext}
                  data-direction="right"
                  ref={nextActionRef}
                  aria-label={nextLabel}
                  type="button"
                  className={styles['sitecore-gallery-action']}
                >
                  <ArrowRightIcon />
                </button>

                <div aria-live={autoplay ? 'off' : 'polite'} className={styles['sitecore-gallery-slides']}>
                  {articles.map((item, index) => (
                    <div
                      role="group"
                      aria-roledescription="slide"
                      key={item.id}
                      data-state={index === selectedIndex ? 'selected' : ''}
                      aria-label={`${index + 1} of ${totalItems} slides`}
                      className={styles['sitecore-gallery-slide-wrapper']}
                    >
                      <ArticleCard.Root key={item.id} className={styles['sitecore-article-card-root']}>
                        <div className={styles['sitecore-article-card-image-wrapper']}>
                          <ArticleCard.Image
                            src={item?.image_url || DEFAULT_IMG_URL}
                            className={styles['sitecore-article-card-image']}
                          />
                        </div>
                        <div className={styles['sitecore-article-card-content']}>
                          <a
                            href={item.url}
                            onClick={(event) => {
                              event.preventDefault();
                              onItemClick({
                                id: item.id,
                                index,
                                sourceId: item.source_id,
                              });
                            }}
                            className={styles['sitecore-article-card-link']}
                          >
                            <ArticleCard.Title className={styles['sitecore-article-card-title']}>
                              {item.name}
                            </ArticleCard.Title>
                            <ArticleCard.Subtitle className={styles['sitecore-article-card-subtitle']}>
                              {item.author}
                            </ArticleCard.Subtitle>
                          </a>
                        </div>
                      </ArticleCard.Root>
                    </div>
                  ))}
                </div>

                {displayIndicators && (
                  <div className={styles['sitecore-gallery-indicators']}>
                    {articles.map((p, index) => (
                      <button
                        key={p.id}
                        data-slide-to={index}
                        aria-label={`Slide ${index + 1}`}
                        data-state={index === selectedIndex ? 'active' : ''}
                        onClick={() => goTo(index)}
                        type="button"
                        className={styles['sitecore-gallery-indicator']}
                      />
                    ))}
                  </div>
                )}
              </section>
            </>
          )}
        </div>
      );
    };
    const RecommendationGalleryWidget = widget(RecommendationGalleryComponent, WidgetDataType.RECOMMENDATION, 'content');
    export default RecommendationGalleryWidget;
    ```

    ```css
    // styles.module.css
    @keyframes rotate-animation {
      from {
        transform: rotate(0deg);
      }
      to {
        transform: rotate(360deg);
      }
    }

    /* ArticleCardRoot */
    .sitecore-article-card-root {
      cursor: pointer;
      box-sizing: border-box;
      box-shadow: 0 0 2px 2px var(--sdc-palette-grey-400);
      background: #fff;
      position: relative;
      transition: 0.15s all ease;
      width: 250px;
    }

    .sitecore-article-card-root:focus-within {
      box-shadow: 0 0 8px 3px rgb(0 0 0 / 15%);
      transform: scale(1.025);
      opacity: 1;
      transition: 0.35s all ease;
    }

    .sitecore-article-card-root:hover {
      box-shadow: 0 0 8px 3px rgb(0 0 0 / 15%);
      transform: scale(1.025);
      opacity: 1;
      transition: 0.35s all ease;
    }

    /* ArticleCardContent */
    .sitecore-article-card-content {
      display: flex;
      height: 150px;
      overflow: hidden;
      padding: var(--sdc-spacing-m);
    }

    /* ArticleCardLink */
    .sitecore-article-card-link {
      color: var(--sdc-palette-text-primary);
      font-size: var(--sdc-typography-fontSize3-fontSize);
      text-decoration: none;
    }

    .sitecore-article-card-link:hover {
      text-decoration: none;
    }

    .sitecore-article-card-link:focus {
      text-decoration: none;
    }

    .sitecore-article-card-link::after {
      position: absolute;
      inset: 0;
      display: block;
      content: ' ';
    }

    /* ArticleCardImage */
    .sitecore-article-card-image {
      width: 100%;
    }

    /* ArticleCardImageWrapper */
    .sitecore-article-card-image-wrapper {
      position: relative;
      height: 120px;
      display: flex;
      justify-content: center;
      align-items: center;
      overflow: hidden;
    }

    /* ArticleCardTitle */
    .sitecore-article-card-title {
      color: var(--sdc-palette-text-primary);
      font-family: var(--sdc-typography-fontFamilySystem);
      font-size: var(--sdc-typography-h3-fontSize);
      font-weight: var(--sdc-typography-h3-fontWeight);
      line-height: var(--sdc-typography-h3-lineHeight);
      margin: 0 0 var(--sdc-spacing-m);
      overflow: hidden;
      display: -webkit-box;
      -webkit-line-clamp: 3;
      -webkit-box-orient: vertical;
    }

    /* ArticleCardSubtitle */
    .sitecore-article-card-subtitle {
      color: var(--sdc-palette-text-secondary);
      font-family: var(--sdc-typography-fontFamilySystem);
      font-size: var(--sdc-typography-fontSize3-fontSize);
      font-weight: var(--sdc-typography-fontSize3-fontWeight);
      line-height: var(--sdc-typography-fontSize3-lineHeight);
      margin: 0 0 var(--sdc-spacing-m);
    }

    /* Title */
    .sitecore-title {
      color: var(--sdc-palette-primary-main);
      font-family: var(--sdc-typography-fontFamilySystem);
      font-size: var(--sdc-typography-fontSize3-fontSize);
    }

    /* LoaderContainer */
    .sitecore-loader-container {
      align-items: center;
      display: flex;
      min-height: 50vh;
    }

    /* LoaderAnimation */
    .sitecore-loader-animation {
      animation: rotate-animation 2s linear infinite;
      display: block;
      fill: var(--sdc-palette-primary-main);
      height: 50px;
      margin: auto;
      width: 50px;
    }

    /* GalleryGrid */
    .sitecore-gallery-grid {
      display: grid;
      grid-template-columns: auto 1fr auto;
      grid-template-areas:
        'startAction slides endAction'
        'indicators indicators indicators';
      -moz-column-gap: 10px;
      column-gap: 10px;
      position: relative;
    }

    /* GallerySlides */
    .sitecore-gallery-slides {
      grid-area: slides;
    }

    /* GalleryAction */
    .sitecore-gallery-action {
      align-self: center;
      background-color: var(--sdc-palette-common-black);
      border: none;
      border-radius: 50%;
      color: var(--sdc-palette-common-white);
      cursor: pointer;
      margin: 0;
      padding: 0;
      width: 50px;
      height: 50px;
    }

    .sitecore-gallery-action[data-direction='right'] {
      grid-area: endAction;
    }

    .sitecore-gallery-action[data-direction='left'] {
      grid-area: startAction;
    }

    .sitecore-gallery-action:focus,
    .sitecore-gallery-action:hover {
      background-color: var(--sdc-palette-primary-main);
      color: var(--sdc-palette-primary-contrastText);
    }

    /* GallerySlideWrapper */
    .sitecore-gallery-slide-wrapper {
      display: none;
      justify-content: center;
    }

    .sitecore-gallery-slide-wrapper[data-state='selected'] {
      display: flex;
    }

    /* GalleryIndicators */
    .sitecore-gallery-indicators {
      display: flex;
      grid-area: indicators;
      justify-content: center;
      margin-top: var(--sdc-spacing-m);
      padding-left: 0;
    }

    /* GalleryIndicator */
    .sitecore-gallery-indicator {
      border: none;
      position: relative;
      flex: 0 1 auto;
      width: 15px;
      height: 3px;
      margin-right: var(--sdc-spacing-s);
      margin-left: var(--sdc-spacing-s);
      text-indent: -999px;
      cursor: pointer;
      background-color: var(--sdc-palette-common-black);
    }

    .sitecore-gallery-indicator[data-state='active'] {
      background-color: var(--sdc-palette-primary-main);
    }

    .sitecore-gallery-indicator::before {
      position: absolute;
      top: -10px;
      left: 0;
      display: inline-block;
      width: 100%;
      height: 10px;
      content: '';
    }

    .sitecore-gallery-indicator::after {
      position: absolute;
      bottom: -10px;
      left: 0;
      display: inline-block;
      width: 100%;
      height: 10px;
      content: '';
    }

    /* RecommendationContainer */
    .sitecore-recommendation-container {
      display: inline-block;
      width: auto;
      min-width: 300px;
    }
    ```

</details>

<details>
<summary><h2>Options</h2></summary>

### Items to display

`itemsToDisplay` sets the maximum number of items displayed in this component.

Default: 6

```tsx
<RecommendationCarousel
	rfkId="rfkid_2"
 	title="Latest news"
 	itemsToDisplay={12}
/>
```

### Autoplay

Set `autoplay` to `true` to display the next recommendation `autoplaySpeed` milliseconds.

```tsx
<RecommendationGallery
	rfkId="rfkid_2"
 	itemsToDisplay={9}
 	autoplay
 	autoplaySpeed={1500}
/>
```

### Item indicators

`displayIndicators` indicate which item is displayed.

```tsx
<RecommendationGallery
	rfkId="rfkid_2"
 	itemsToDisplay={9}
 	displayIndicators
/>
```

</details>