# Wheel Carousel

> A rotating-wheel picker with inertial drag and scroll, curved fading labels, and a crossfading image for the active item.

A rotating-wheel picker. Labels sit on the rim of a large wheel and fade as they curve away; the active item's photo crossfades in beside it. Drag up or down, scroll with the mouse wheel, or focus it and use the arrow keys, Home and End.

<demo-wheel-carousel>



</demo-wheel-carousel>

## Installation

```bash
npx shadcn-vue@latest add "https://nxui.geoql.in/r/wheel-carousel.json"
```

## Usage

```vue
<script setup lang="ts">
  import { ref } from 'vue';
  import WheelCarousel from '~/components/ui/wheel-carousel/WheelCarousel.vue';
  import type { WheelCarouselItem } from '~/components/ui/wheel-carousel/types';

  const projects: WheelCarouselItem[] = [
    {
      label: 'Canyon House',
      image: '/images/projects/canyon-house.jpg',
      imageAlt: 'Modern concrete home built into a desert canyon',
    },
    {
      label: 'Harbor Loft',
      image: '/images/projects/harbor-loft.jpg',
      imageAlt: 'Bright coastal loft with large windows',
    },
    {
      label: 'Glass Pavilion',
      image: '/images/projects/glass-pavilion.jpg',
      imageAlt: 'Minimal glass pavilion surrounded by trees',
    },
  ];

  const active = ref(0);
</script>

<template>
  <div class="h-170 w-full">
    <WheelCarousel
      v-model:active-index="active"
      :items="projects"
      photo-side="right"
      photo-aspect="4/3"
      :photo-width="30"
      :gap="24"
      :radius="260"
      :spacing="13"
      :visible-items="6"
    />
  </div>
</template>
```

In `system` mode the palette follows the site's color mode (white or black canvas). `light` and `dark` pin one of them; `custom` uses the `background`, `panel-color`, `text-color`, `selected-color` and `marker-color` props.

## Props

<table>
<thead>
  <tr>
    <th>
      Prop
    </th>
    
    <th>
      Type
    </th>
    
    <th>
      Default
    </th>
    
    <th>
      Description
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        items
      </code>
    </td>
    
    <td>
      <code>
        WheelCarouselItem[]
      </code>
    </td>
    
    <td>
      24 sample projects
    </td>
    
    <td>
      Labels and photos. Each item takes <code>
        label
      </code>
      
      , <code>
        image
      </code>
      
      , <code>
        imageAlt?
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        mode
      </code>
    </td>
    
    <td>
      <code>
        'system' | 'light' | 'dark' | 'custom'
      </code>
    </td>
    
    <td>
      <code>
        'system'
      </code>
    </td>
    
    <td>
      Automatic, fixed, or custom colors
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        photo-side
      </code>
    </td>
    
    <td>
      <code>
        'left' | 'right'
      </code>
    </td>
    
    <td>
      <code>
        'left'
      </code>
    </td>
    
    <td>
      Side of the wheel that holds the active image
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        photo-width
      </code>
    </td>
    
    <td>
      <code>
        number
      </code>
    </td>
    
    <td>
      <code>
        24
      </code>
    </td>
    
    <td>
      Image column width (%)
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        photo-aspect
      </code>
    </td>
    
    <td>
      <code>
        '3/4' | '1/1' | '4/3' | '3/2'
      </code>
    </td>
    
    <td>
      <code>
        '3/4'
      </code>
    </td>
    
    <td>
      Aspect ratio of the active image
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        photo-radius
      </code>
    </td>
    
    <td>
      <code>
        number
      </code>
    </td>
    
    <td>
      <code>
        14
      </code>
    </td>
    
    <td>
      Corner radius of the image (px)
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        content-width
      </code>
    </td>
    
    <td>
      <code>
        number
      </code>
    </td>
    
    <td>
      <code>
        900
      </code>
    </td>
    
    <td>
      Max inner content width (px)
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        gap
      </code>
    </td>
    
    <td>
      <code>
        number
      </code>
    </td>
    
    <td>
      <code>
        0
      </code>
    </td>
    
    <td>
      Space between image column and wheel (px)
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        radius
      </code>
    </td>
    
    <td>
      <code>
        number
      </code>
    </td>
    
    <td>
      <code>
        320
      </code>
    </td>
    
    <td>
      Radius of the label wheel (px)
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        spacing
      </code>
    </td>
    
    <td>
      <code>
        number
      </code>
    </td>
    
    <td>
      <code>
        14
      </code>
    </td>
    
    <td>
      Angle between labels (deg)
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        visible-items
      </code>
    </td>
    
    <td>
      <code>
        number
      </code>
    </td>
    
    <td>
      <code>
        7
      </code>
    </td>
    
    <td>
      Labels visible on each side of the selection
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        apex-inset
      </code>
    </td>
    
    <td>
      <code>
        number
      </code>
    </td>
    
    <td>
      <code>
        34
      </code>
    </td>
    
    <td>
      Horizontal position of the selection point (%)
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        snap
      </code>
    </td>
    
    <td>
      <code>
        boolean
      </code>
    </td>
    
    <td>
      <code>
        true
      </code>
    </td>
    
    <td>
      Settle on the nearest whole item
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        momentum
      </code>
    </td>
    
    <td>
      <code>
        boolean
      </code>
    </td>
    
    <td>
      <code>
        true
      </code>
    </td>
    
    <td>
      Keep drag and wheel velocity after input ends
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        appear
      </code>
    </td>
    
    <td>
      <code>
        boolean
      </code>
    </td>
    
    <td>
      <code>
        true
      </code>
    </td>
    
    <td>
      Play an entrance animation on mount
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        crossfade-duration
      </code>
    </td>
    
    <td>
      <code>
        number
      </code>
    </td>
    
    <td>
      <code>
        0.5
      </code>
    </td>
    
    <td>
      Image crossfade duration (s)
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        scroll-speed
      </code>
    </td>
    
    <td>
      <code>
        number
      </code>
    </td>
    
    <td>
      <code>
        0.008
      </code>
    </td>
    
    <td>
      Rotation per wheel delta pixel
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        drag-speed
      </code>
    </td>
    
    <td>
      <code>
        number
      </code>
    </td>
    
    <td>
      <code>
        0.02
      </code>
    </td>
    
    <td>
      Rotation per dragged pixel
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        background
      </code>
    </td>
    
    <td>
      <code>
        string
      </code>
    </td>
    
    <td>
      <code>
        'rgb(255, 246, 236)'
      </code>
    </td>
    
    <td>
      Canvas color in <code>
        custom
      </code>
      
       mode
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        panel-color
      </code>
    </td>
    
    <td>
      <code>
        string
      </code>
    </td>
    
    <td>
      <code>
        background
      </code>
    </td>
    
    <td>
      Image placeholder color in <code>
        custom
      </code>
      
       mode
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        text-color
      </code>
    </td>
    
    <td>
      <code>
        string
      </code>
    </td>
    
    <td>
      <code>
        'rgba(180, 90, 20, 0.45)'
      </code>
    </td>
    
    <td>
      Inactive label color in <code>
        custom
      </code>
      
       mode
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        selected-color
      </code>
    </td>
    
    <td>
      <code>
        string
      </code>
    </td>
    
    <td>
      <code>
        'rgb(180, 84, 30)'
      </code>
    </td>
    
    <td>
      Active label color in <code>
        custom
      </code>
      
       mode
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        show-marker
      </code>
    </td>
    
    <td>
      <code>
        boolean
      </code>
    </td>
    
    <td>
      <code>
        true
      </code>
    </td>
    
    <td>
      Show the dot at the selection point
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        marker-color
      </code>
    </td>
    
    <td>
      <code>
        string
      </code>
    </td>
    
    <td>
      <code>
        'rgb(232, 121, 46)'
      </code>
    </td>
    
    <td>
      Dot color in <code>
        custom
      </code>
      
       mode
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        marker-size
      </code>
    </td>
    
    <td>
      <code>
        number
      </code>
    </td>
    
    <td>
      <code>
        16
      </code>
    </td>
    
    <td>
      Dot diameter (px)
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        marker-gap
      </code>
    </td>
    
    <td>
      <code>
        number
      </code>
    </td>
    
    <td>
      <code>
        20
      </code>
    </td>
    
    <td>
      Distance between dot and labels (px)
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        edge-fade
      </code>
    </td>
    
    <td>
      <code>
        boolean
      </code>
    </td>
    
    <td>
      <code>
        true
      </code>
    </td>
    
    <td>
      Fade labels at the top and bottom edges
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        edge-fade-size
      </code>
    </td>
    
    <td>
      <code>
        number
      </code>
    </td>
    
    <td>
      <code>
        30
      </code>
    </td>
    
    <td>
      Size of the edge fade (%)
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        active-index
      </code>
    </td>
    
    <td>
      <code>
        number
      </code>
    </td>
    
    <td>
      —
    </td>
    
    <td>
      Controlled active index (supports <code>
        v-model:active-index
      </code>
      
      )
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        initial-index
      </code>
    </td>
    
    <td>
      <code>
        number
      </code>
    </td>
    
    <td>
      <code>
        0
      </code>
    </td>
    
    <td>
      Initially selected item when uncontrolled
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        class
      </code>
    </td>
    
    <td>
      <code>
        string
      </code>
    </td>
    
    <td>
      <code>
        ''
      </code>
    </td>
    
    <td>
      Classes for the outer container
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        photo-class
      </code>
    </td>
    
    <td>
      <code>
        string
      </code>
    </td>
    
    <td>
      <code>
        ''
      </code>
    </td>
    
    <td>
      Classes for the image frame
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        item-class
      </code>
    </td>
    
    <td>
      <code>
        string
      </code>
    </td>
    
    <td>
      <code>
        ''
      </code>
    </td>
    
    <td>
      Classes for each wheel label
    </td>
  </tr>
</tbody>
</table>

## Events

<table>
<thead>
  <tr>
    <th>
      Event
    </th>
    
    <th>
      Payload
    </th>
    
    <th>
      Description
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        active-change
      </code>
    </td>
    
    <td>
      <code>
        (item: WheelCarouselItem, index: number)
      </code>
    </td>
    
    <td>
      Fires when the active item changes
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        update:active-index
      </code>
    </td>
    
    <td>
      <code>
        (index: number)
      </code>
    </td>
    
    <td>
      <code>
        v-model:active-index
      </code>
      
       update
    </td>
  </tr>
</tbody>
</table>

## Accessibility

- The wheel is a focusable `listbox`; each visible label is an `option` and `aria-activedescendant` tracks the selection.
- Arrow keys move one item, Home and End jump to the first and last item.
- A polite live region announces the active label and its position.
- With `prefers-reduced-motion`, the entrance animation, momentum and crossfade are removed and the wheel snaps at once.

## Credits

- Ported from [componentry.fun](https://componentry.dev/docs/components/wheel-carousel) by Harsh Jadhav.
