Skip to content

Commit ee61b7a

Browse files
zernoniaclaude
andauthored
feat(Rating): expose Rating component and add documentation (#2691)
Export `RatingRoot`, `RatingItem`, and `RatingItemIndicator` from the public package entry point, register them in the components constant, and add a full documentation page with demos and auto-generated API meta. Also renames the `RatingItemIndicator` props interface to `RatingItemIndicatorProps` (was mistakenly `RatingItemProps`) and tidies up the JSDoc on the root props. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 43989e8 commit ee61b7a

15 files changed

Lines changed: 602 additions & 8 deletions

File tree

docs/.vitepress/config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,7 @@ export default defineConfig({
154154
{ text: 'Label', link: '/docs/components/label' },
155155
{ text: 'Pin Input', link: '/docs/components/pin-input' },
156156
{ text: 'Radio Group', link: '/docs/components/radio-group' },
157+
{ text: `Rating ${BadgeHTML('Alpha', true)}`, link: '/docs/components/rating' },
157158
{ text: 'Select', link: '/docs/components/select' },
158159
{ text: 'Slider', link: '/docs/components/slider' },
159160
{ text: 'Switch', link: '/docs/components/switch' },
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<script setup lang="ts">
2+
import { Icon } from '@iconify/vue'
3+
import { RatingItem, RatingItemIndicator, RatingRoot } from 'reka-ui'
4+
import { ref } from 'vue'
5+
import './styles.css'
6+
7+
const rating = ref(3)
8+
</script>
9+
10+
<template>
11+
<RatingRoot
12+
v-slot="{ items }"
13+
v-model="rating"
14+
hoverable
15+
class="RatingRoot"
16+
>
17+
<RatingItem
18+
v-for="item in items"
19+
:key="item"
20+
v-slot="{ steps }"
21+
:item="item"
22+
class="RatingItem"
23+
>
24+
<RatingItemIndicator
25+
v-for="step in steps"
26+
:key="step"
27+
:step="step"
28+
:aria-label="`Rate ${step}`"
29+
class="RatingItemIndicator"
30+
>
31+
<Icon
32+
icon="radix-icons:star"
33+
class="RatingStar"
34+
/>
35+
</RatingItemIndicator>
36+
</RatingItem>
37+
</RatingRoot>
38+
</template>
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
.RatingRoot {
2+
display: flex;
3+
gap: 4px;
4+
}
5+
6+
.RatingItem {
7+
position: relative;
8+
width: 28px;
9+
height: 28px;
10+
cursor: pointer;
11+
}
12+
13+
.RatingItemIndicator {
14+
position: absolute;
15+
overflow: hidden;
16+
border-radius: 4px;
17+
color: var(--gray-6, #d7d3d0);
18+
width: var(--reka-rating-item-step-width);
19+
opacity: var(--reka-rating-item-step-opacity);
20+
z-index: var(--reka-rating-item-step-z-index);
21+
}
22+
23+
.RatingItemIndicator[data-state="active"] {
24+
color: #facc15;
25+
}
26+
27+
.RatingItemIndicator:focus-visible {
28+
outline: 2px solid #facc15;
29+
outline-offset: 2px;
30+
}
31+
32+
.RatingStar {
33+
width: 28px;
34+
height: 28px;
35+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<script setup lang="ts">
2+
import { Icon } from '@iconify/vue'
3+
import { RatingItem, RatingItemIndicator, RatingRoot } from 'reka-ui'
4+
import { ref } from 'vue'
5+
6+
const rating = ref(3)
7+
</script>
8+
9+
<template>
10+
<RatingRoot
11+
v-slot="{ items }"
12+
v-model="rating"
13+
hoverable
14+
class="flex gap-1"
15+
>
16+
<RatingItem
17+
v-for="item in items"
18+
:key="item"
19+
v-slot="{ steps }"
20+
:item="item"
21+
class="relative size-7 cursor-pointer"
22+
>
23+
<RatingItemIndicator
24+
v-for="step in steps"
25+
:key="step"
26+
:step="step"
27+
:aria-label="`Rate ${step}`"
28+
class="absolute overflow-hidden text-stone-300 data-[state=active]:text-yellow-400 w-[var(--reka-rating-item-step-width)] opacity-[var(--reka-rating-item-step-opacity)] z-[var(--reka-rating-item-step-z-index)] focus:outline-none focus-visible:ring-2 focus-visible:ring-yellow-400 rounded"
29+
>
30+
<Icon
31+
icon="radix-icons:star"
32+
class="size-7"
33+
/>
34+
</RatingItemIndicator>
35+
</RatingItem>
36+
</RatingRoot>
37+
</template>
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
/** @type {import('tailwindcss').Config} */
2+
module.exports = {
3+
content: ['./**/*.vue'],
4+
theme: {
5+
extend: {},
6+
},
7+
plugins: [],
8+
}
Lines changed: 258 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,258 @@
1+
---
2+
3+
title: Rating
4+
description: A star-rating input where the user selects a score, with support for fractional values.
5+
name: rating
6+
aria: https://www.w3.org/WAI/ARIA/apg/patterns/radio
7+
---
8+
9+
# Rating
10+
11+
<Description>
12+
A star-rating input where the user selects a score, with support for fractional values.
13+
</Description>
14+
15+
<ComponentPreview name="Rating" />
16+
17+
## Features
18+
19+
<Highlights
20+
:features="[
21+
'Can be controlled or uncontrolled.',
22+
'Supports fractional ratings (half, quarter, etc.) via the step prop.',
23+
'Previews the value under the pointer on hover.',
24+
'Can be cleared by clicking the active value again.',
25+
'Built on top of Radio Group — full keyboard navigation and form support.',
26+
'Supports Right to Left direction.',
27+
'Exposes CSS variables for rendering partial steps.',
28+
]"
29+
/>
30+
31+
## Installation
32+
33+
Install the component from your command line.
34+
35+
<InstallationTabs value="reka-ui" />
36+
37+
## Anatomy
38+
39+
Import all parts and piece them together. `RatingRoot` exposes the list of `items`, and each `RatingItem` exposes the `steps` it is made of, so you render an indicator per step.
40+
41+
```vue
42+
<script setup>
43+
import { RatingItem, RatingItemIndicator, RatingRoot } from 'reka-ui'
44+
</script>
45+
46+
<template>
47+
<RatingRoot v-slot="{ items }">
48+
<RatingItem
49+
v-for="item in items"
50+
:key="item"
51+
v-slot="{ steps }"
52+
:item="item"
53+
>
54+
<RatingItemIndicator
55+
v-for="step in steps"
56+
:key="step"
57+
:step="step"
58+
/>
59+
</RatingItem>
60+
</RatingRoot>
61+
</template>
62+
```
63+
64+
## API Reference
65+
66+
### Root
67+
68+
Contains all the parts of a rating and provides the rating state. Renders a [Radio Group](/docs/components/radio-group) under the hood, so it supports form submission and keyboard navigation.
69+
70+
<!-- @include: @/meta/RatingRoot.md -->
71+
72+
<DataAttributesTable
73+
:data="[
74+
{
75+
attribute: '[data-disabled]',
76+
values: 'Present when disabled',
77+
},
78+
{
79+
attribute: '[data-orientation]',
80+
values: ['vertical', 'horizontal'],
81+
},
82+
]"
83+
/>
84+
85+
### Item
86+
87+
Wraps a single rating value (e.g. a single star). It computes the list of `steps` that compose this item based on the root's `step` prop, exposed via the default slot. Renders a `label` by default.
88+
89+
<!-- @include: @/meta/RatingItem.md -->
90+
91+
### ItemIndicator
92+
93+
The interactive indicator rendered for each step of an item. It reflects whether the step is active based on the current (or hovered) value.
94+
95+
<!-- @include: @/meta/RatingItemIndicator.md -->
96+
97+
<DataAttributesTable
98+
:data="[
99+
{
100+
attribute: '[data-state]',
101+
values: ['active'],
102+
},
103+
{
104+
attribute: '[data-disabled]',
105+
values: 'Present when disabled',
106+
},
107+
]"
108+
/>
109+
110+
To render partial steps (for `step` values below `1`), `RatingItemIndicator` exposes the following CSS variables:
111+
112+
| CSS Variable | Description |
113+
| ---------------------------------- | ------------------------------------------------------------------------------------ |
114+
| `--reka-rating-item-step-width` | The width this step should occupy within the item, e.g. `50%` for a half step. |
115+
| `--reka-rating-item-step-opacity` | `1` when the step should be visible, `0` otherwise (used to stack overlapping steps). |
116+
| `--reka-rating-item-step-z-index` | The stacking order of the step so smaller steps render above larger ones. |
117+
118+
A typical fractional indicator clips its width and stacks steps using these variables:
119+
120+
```vue
121+
<RatingItemIndicator
122+
:step="step"
123+
class="absolute overflow-hidden w-[var(--reka-rating-item-step-width)] opacity-[var(--reka-rating-item-step-opacity)] z-[var(--reka-rating-item-step-z-index)]"
124+
/>
125+
```
126+
127+
## Examples
128+
129+
### Fractional rating
130+
131+
Use the `step` prop to allow values smaller than `1`. Each `RatingItem` is then split into multiple `steps`, and each step renders its own indicator clipped with the exposed CSS variables.
132+
133+
```vue line=10
134+
<script setup>
135+
import { RatingItem, RatingItemIndicator, RatingRoot } from 'reka-ui'
136+
import { ref } from 'vue'
137+
138+
const rating = ref(2.5)
139+
</script>
140+
141+
<template>
142+
<RatingRoot
143+
v-slot="{ items }"
144+
v-model="rating"
145+
:step="0.5"
146+
>
147+
<RatingItem
148+
v-for="item in items"
149+
:key="item"
150+
v-slot="{ steps }"
151+
:item="item"
152+
class="relative"
153+
>
154+
<RatingItemIndicator
155+
v-for="step in steps"
156+
:key="step"
157+
:step="step"
158+
class="absolute overflow-hidden w-[var(--reka-rating-item-step-width)] opacity-[var(--reka-rating-item-step-opacity)] z-[var(--reka-rating-item-step-z-index)]"
159+
/>
160+
</RatingItem>
161+
</RatingRoot>
162+
</template>
163+
```
164+
165+
### Clearable
166+
167+
Use the `clearable` prop to let users reset the rating to `0` by clicking the currently selected value again.
168+
169+
```vue line=4
170+
<template>
171+
<RatingRoot
172+
v-model="rating"
173+
clearable
174+
>
175+
<!-- ... -->
176+
</RatingRoot>
177+
</template>
178+
```
179+
180+
### Hover preview
181+
182+
Use the `hoverable` prop to preview the value under the pointer before committing to it. The `RatingItemIndicator` exposes `data-state="active"` for every step at or below the hovered value.
183+
184+
```vue line=4
185+
<template>
186+
<RatingRoot
187+
v-model="rating"
188+
hoverable
189+
>
190+
<!-- ... -->
191+
</RatingRoot>
192+
</template>
193+
```
194+
195+
### Custom length
196+
197+
Use the `length` prop to change how many items are rendered. It defaults to `5`.
198+
199+
```vue line=4
200+
<template>
201+
<RatingRoot
202+
v-model="rating"
203+
:length="10"
204+
>
205+
<!-- ... -->
206+
</RatingRoot>
207+
</template>
208+
```
209+
210+
### Read-only / disabled
211+
212+
Use the `disabled` prop to prevent interaction, for example when displaying an average rating.
213+
214+
```vue line=4
215+
<template>
216+
<RatingRoot
217+
:default-value="4"
218+
disabled
219+
>
220+
<!-- ... -->
221+
</RatingRoot>
222+
</template>
223+
```
224+
225+
## Accessibility
226+
227+
Built on top of the [Radio Group](/docs/components/radio-group) primitive and adheres to the [Radio Group WAI-ARIA design pattern](https://www.w3.org/WAI/ARIA/apg/patterns/radio). Provide an accessible label for each step via `aria-label` so screen reader users understand the value each indicator represents.
228+
229+
### Keyboard Interactions
230+
231+
<KeyboardTable
232+
:data="[
233+
{
234+
keys: ['Tab'],
235+
description: 'Moves focus to either the checked item or the first item in the rating.',
236+
},
237+
{
238+
keys: ['Space'],
239+
description: 'When focus is on an unchecked item, selects that value.',
240+
},
241+
{
242+
keys: ['ArrowDown'],
243+
description: 'Moves focus and selection to the next item.',
244+
},
245+
{
246+
keys: ['ArrowRight'],
247+
description: 'Moves focus and selection to the next item.',
248+
},
249+
{
250+
keys: ['ArrowUp'],
251+
description: 'Moves focus and selection to the previous item.',
252+
},
253+
{
254+
keys: ['ArrowLeft'],
255+
description: 'Moves focus and selection to the previous item.',
256+
},
257+
]"
258+
/>

0 commit comments

Comments
 (0)