# Carousel Component

A flexible slide carousel powered by Splide. Accepts any content as slides — suitable for image showcases, testimonial sliders, logo carousels, feature rotators, and more. Supports standard paged navigation as well as a continuous ticker (auto-scroll) mode.

## Import path
```tsx
import Carousel from '@hubspot/cms-component-library/Carousel';
```

## Purpose

The Carousel component wraps Splide to provide a consistent, accessible carousel with sensible defaults. It accepts arbitrary React children as slides, so each slide can contain any content — images, text, other library components, etc. Behavior is controlled entirely via props: transition type, autoplay, ticker mode, per-page count, gap, and navigation visibility.

## Component Structure

```
Carousel/
├── index.tsx                     # Main component with Splide integration
├── types.ts                      # TypeScript type definitions
├── splide.d.ts                   # Type shim for @splidejs/react-splide
├── index.module.scss             # CSS module for arrows, pagination, and layout
└── stories/
    ├── Carousel.stories.tsx      # Storybook usage examples
    └── Carousel.stories.module.scss  # Story-only slide styles
```

## Components

### Carousel

**Purpose:** Renders a Splide carousel where each direct child becomes a slide.

**Props:**
```tsx
{
  type?: 'loop' | 'slide' | 'fade';  // Transition type (default: 'loop')
  perPage?: number;                  // Slides visible at once (default: 1)
  gap?: number | string;             // Space between slides, e.g. 16 or '1rem' (default: none)
  autoplay?: boolean;                // Enable timed auto-advance (default: false)
  autoplayInterval?: number;         // Milliseconds between auto-advances (default: 3000)
  ticker?: boolean;                  // Enable continuous pixel-by-pixel scroll (default: false)
  tickerSpeed?: number;              // Scroll speed in px/frame when ticker is true (default: 1)
  showArrows?: boolean;              // Show prev/next arrow buttons (default: true)
  showPagination?: boolean;          // Show pagination dots (default: true)
  className?: string;                // Additional CSS classes for the root element
  style?: React.CSSProperties;       // Inline styles for the root element
  children?: React.ReactNode;        // Slide content — each child becomes one slide
}
```

**Notes:**
- `ticker` and `autoplay` are mutually exclusive. When `ticker` is true, `autoplay`, `showArrows`, and `showPagination` are ignored — the ticker runs continuously with no discrete navigation.
- `type: 'fade'` only works meaningfully with `perPage: 1`.
- `gap` accepts any value Splide supports: a unitless number (pixels) or a CSS string like `'1rem'` or `'16px'`.

## Usage Examples

### Island Usage

The Carousel component uses browser APIs (via Splide) and must be used inside an Island when integrated into a HubSpot CMS module.

#### Island Component

```tsx
// islands/CarouselIsland.tsx
import Carousel from '@hubspot/cms-component-library/Carousel';

type Slide = {
  heading: string;
  body: string;
};

export default function CarouselIsland({ slides }: { slides: Slide[] }) {
  return (
    <Carousel type="loop" showArrows showPagination>
      {slides.map((slide, i) => (
        <div key={i} style={{ padding: '48px' }}>
          <h2>{slide.heading}</h2>
          <p>{slide.body}</p>
        </div>
      ))}
    </Carousel>
  );
}
```

#### Module File

```tsx
import { Island } from '@hubspot/cms-components';
// @ts-expect-error -- ?island not typed
import CarouselIsland from './islands/CarouselIsland.js?island';

export const Component = ({ fieldValues }) => {
  return (
    <Island
      module={CarouselIsland}
      slides={fieldValues.slides}
    />
  );
};
```

### Standard Carousel (loop)

```tsx
<Carousel>
  <img src="/slide1.jpg" alt="Slide 1" />
  <img src="/slide2.jpg" alt="Slide 2" />
  <img src="/slide3.jpg" alt="Slide 3" />
</Carousel>
```

### Testimonial Slider

```tsx
<Carousel showArrows showPagination>
  {testimonials.map(t => (
    <div key={t.name}>
      <p>"{t.quote}"</p>
      <strong>{t.name}</strong>
    </div>
  ))}
</Carousel>
```

### Fade Transition with Autoplay

```tsx
<Carousel type="fade" autoplay autoplayInterval={4000} showPagination>
  <FeatureSlide {...features[0]} />
  <FeatureSlide {...features[1]} />
  <FeatureSlide {...features[2]} />
</Carousel>
```

### Multi-Slide per Page with Gap

```tsx
<Carousel perPage={3} gap="24px" showArrows>
  {items.map(item => (
    <CardSlide key={item.id} {...item} />
  ))}
</Carousel>
```

### Logo Ticker (continuous scroll)

```tsx
<Carousel ticker tickerSpeed={0.75} perPage={4}>
  {logos.map(logo => (
    <LogoSlide key={logo.name} {...logo} />
  ))}
</Carousel>
```

## HubSpot CMS Integration

### Field Definitions

The Carousel component does not ship with pre-built field definitions. Define fields in your module based on what the slides contain. A typical pattern is a `RepeatedFieldGroup` for slide content paired with individual fields for carousel behavior.

```tsx
import { ModuleFields, RepeatedFieldGroup, BooleanField, NumberField } from '@hubspot/cms-components/fields';

export const fields = (
  <ModuleFields>
    <RepeatedFieldGroup
      label="Slides"
      name="slides"
      occurrence={{ min: 1, max: 12, default: 3 }}
    >
      {/* your per-slide fields */}
    </RepeatedFieldGroup>
    <BooleanField label="Autoplay" name="autoplay" display="toggle" default={false} />
    <NumberField label="Autoplay interval (ms)" name="autoplayInterval" min={500} default={3000} />
  </ModuleFields>
);
```

### Module Usage Example

```tsx
import { Island } from '@hubspot/cms-components';
// @ts-expect-error -- ?island not typed
import CarouselIsland from './islands/CarouselIsland.js?island';

type CarouselModuleProps = {
  fieldValues: {
    slides?: Array<{ heading?: string; body?: string }>;
    autoplay?: boolean;
    autoplayInterval?: number;
  };
};

export const Component = ({ fieldValues }: CarouselModuleProps) => {
  return (
    <Island
      module={CarouselIsland}
      slides={fieldValues.slides ?? []}
      autoplay={fieldValues.autoplay ?? false}
      autoplayInterval={fieldValues.autoplayInterval ?? 3000}
    />
  );
};
```

## Styling

Default styles are hardcoded in `index.module.scss`: white circle arrows (40px, subtle shadow and border), dark active pagination pill, gray inactive dots, 16px gap between dots and slides. There are no exposed CSS custom properties.

To override styles, pass a `className` prop. All Splide class rules in the stylesheet use `:where()` to keep specificity at `(0,1,0)`, so a single extra class selector in consumer CSS is always sufficient to win:

```css
/* Your stylesheet */
.my-carousel .splide__arrow {
  background: rgba(0, 0, 0, 0.5);
}

.my-carousel .splide__pagination__page {
  background: rgba(255, 255, 255, 0.4);
}

.my-carousel .splide__pagination__page.is-active {
  background: #fff;
}
```

```tsx
<Carousel className="my-carousel">
  {slides}
</Carousel>
```

## Accessibility

- **Keyboard Navigation**: Splide provides built-in keyboard support — arrow keys navigate between slides when the carousel is focused
- **Reduced Motion**: Splide's `reducedMotion` option is set to disable speed and autoplay for users with `prefers-reduced-motion`. CSS transitions on arrows and dots are also disabled via a `@media (prefers-reduced-motion: reduce)` rule
- **Arrow buttons**: Rendered as `<button>` elements with Splide's built-in screen reader labels
- **Ticker mode**: Disables drag interaction and navigation controls, suitable for purely decorative logo strips — consider `aria-hidden` on the ticker wrapper if the content is decorative

## Best Practices

- **Use Islands**: The Carousel requires JavaScript and must be used inside a module Island
- **Keep slide content serializable**: Since the Island boundary is at the module level, ensure all data passed to the Island as props is JSON-serializable; the slide rendering happens inside the Island
- **Ticker vs autoplay**: Use `ticker` for continuous logo/brand strips where no navigation is needed. Use `autoplay` for content where users may want to pause and read
- **Avoid `type="fade"` with `perPage > 1`**: Fade transition only makes sense with a single slide visible at a time
- **Provide enough slides for loop mode**: `type="loop"` requires enough slides to fill the track — Splide will clone slides automatically, but very few slides with high `perPage` can produce visual artifacts
- **Gap with perPage**: `gap` is most useful when `perPage > 1` to create visual separation between visible slides
