Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

https://github.com/smastrom/react-rating

:star: Zero-dependency, highly customizable rating component for React.
https://github.com/smastrom/react-rating

rating rating-star rating-stars react react-component react-rating reviews star stars

Last synced: 2 months ago
JSON representation

:star: Zero-dependency, highly customizable rating component for React.

Lists

README

        

![react-rating-version](https://img.shields.io/npm/v/@smastrom/react-rating?color=22C55E) ![react-rating-build-workflow](https://img.shields.io/github/actions/workflow/status/smastrom/react-rating/build.yml?branch=main&color=22C55E)
![react-rating-tests-workflow](https://img.shields.io/github/actions/workflow/status/smastrom/react-rating/tests.yml?branch=main&color=22C55E&label=tests) ![react-rating-coverage](https://img.shields.io/codecov/c/github/smastrom/react-rating?color=22C55E) ![react-rating-dependencies](https://img.shields.io/badge/dependency%20count-0-22C55E)

# React Rating

Zero dependency, highly customizable rating component for React.

![react-rating](https://i.ibb.co/L6M0hfw/new.png)

[Demo and Examples](https://reactrating.netlify.app) β€” [NextJS Page Router](https://stackblitz.com/edit/nextjs-5qw9id?file=pages/index.tsx) β€” [Vite](https://stackblitz.com/edit/vitejs-vite-gwqytd?file=src/App.tsx)


## Features

- **Use any SVG**: No headaches, icon fonts or packages to install in order to use your favorite shapes.
- Endless possibilities of customization
- Most common rating shapes included
- Zero-config smart half-fill
- Dead simple per-active-item styling
- Built with accessibility in mind
- Truly responsive and mobile-first
- Controllable with React Hook Form
- Simple DOM structure
- Zero-config RTL support
- Works with SSR


## Installation

```zsh
pnpm add @smastrom/react-rating
```

```zsh
yarn add @smastrom/react-rating
```

```zsh
npm i @smastrom/react-rating
```


## Usage

### 1. Import CSS and Rating component

```jsx
import { Rating } from '@smastrom/react-rating'

import '@smastrom/react-rating/style.css'
```

> Importing the CSS **only once** (most likely _main.js_ or _App.jsx_) is enough to use Rating in any other component of your App.

Remix

**app/root.tsx**

```tsx
import styles from '@smastrom/react-rating/style.css';

export function links() {
return [{ rel: 'stylesheet', href: styles }];
}

export default function App() {
// ...
```

**in any page.tsx**

```tsx
import { Rating } from '@smastrom/react-rating';

export default function Index() {
// ...
```

NextJS 13 - App Router

### Interactive rating

**app/layout.tsx**

```tsx
import '@smastrom/react-rating/style.css'
```

**components/Rating.tsx**

```tsx
'use client'

import { useState } from 'react'
import { Rating as ReactRating } from '@smastrom/react-rating'

export function Rating() {
const [rating, setRating] = useState(0)

return
}
```

**in any page/component:**

```tsx
import { Rating } from './components/Rating'

export default function Home() {
return (


{/* Other nodes... */}

{/* Other nodes... */}

)
}
```

### Non-interactive rating

**app/layout.tsx**

```tsx
import '@smastrom/react-rating/style.css'
```

**in any page/component:**

```tsx
import { Rating } from '@smastrom/react-rating'

export default function Home() {
return (


{/* Other nodes... */}

{/* Other nodes... */}

)
}
```

NextJS - Pages Router


**pages/\_app.js**

```jsx
import '@smastrom/react-rating/style.css';

function MyApp({ Component, pageProps }) {
// ...
```

**in any page/component:**

```tsx
import { Rating } from '@smastrom/react-rating';

export default function Home() {
// ...
```

Gatsby


**gatsby-browser.js** - Create the file at the root of your project if it doesn't exist, and relaunch the dev server.

```jsx
import '@smastrom/react-rating/style.css'
```

**in any page/component:**

```tsx
import { Rating } from '@smastrom/react-rating';

const IndexPage = () => {
// ...
```

Vite


**main.jsx**

```jsx
import '@smastrom/react-rating/style.css'
```

**in any component:**

```jsx
import { Rating } from '@smastrom/react-rating';

function App() {
// ...
```


### 2. Give it a max-width and init the state

Since **Rating** will span across the entire container, define a _maximum width_ via inline styles or css class:

```jsx
function App() {
const [rating, setRating] = useState(0) // Initial value

return
}
```

CSS Class

**my-styles.css**

```css
.my-rating-class {
max-width: 600px;
}
```

**App.jsx**

```jsx
import './my-styles.css'

function App() {
const [rating, setRating] = useState(0)

return
}
```


## API

**Legend**

| Color | Description |
| ------------------- | ------------------------------------------ |
| :green_circle: | Has always effect |
| :large_blue_circle: | Has effect only if `readOnly` is **false** |
| :purple_circle: | Has effect only if `readOnly` is **true** |


### :cyclone: Core

| :thinking: | Prop | Description | Type | Default | Required |
| ------------------- | ---------------- | ------------------------------------------------------------------------------ | ----------------------------------------------- | --------- | ------------------------------- |
| :green_circle: | `value` | An integer from 0 to `items`. It can be a float if `readOnly` is **true**. | number | undefined | :white_check_mark: |
| :large_blue_circle: | `onChange` | Setter or custom function to update the rating. | RatingChange | () => {} | Only if `readOnly` is **false** |
| :large_blue_circle: | `onHoverChange` | Callback to execute while navigating the rating items. | (hoveredValue: number) => void | () => {} | :x: |
| :green_circle: | `items` | Rating items to display. | 1 \| 2 \| 3 \| 4 \| 5 \| 6 \| 7 \| 8 \| 9 \| 10 | 5 | :x: |
| :green_circle: | `readOnly` | Whether to render an accessible image element or not. | boolean | false | :x: |
| :large_blue_circle: | `isDisabled` | Whether to disable the radio group or not. | boolean | false | :x: |
| :large_blue_circle: | `isRequired` | Whether users should be able to set rating to 0 or not. | boolean | false | :x: |
| :green_circle: | `preventDefault` | Whether or not to call `event.preventDefault` on click and Enter/Space select. | `click` \| `keydown` \| `all` \| `none` | `all` | :x: |

`ref`, `id`, `className`, `style`, `onBlur`, `onFocus` are also available.


### :nail_care: Appearance

| :thinking: | Prop | Description | Type | Default | Required |
| ------------------- | ----------------------- | ---------------------------------------------------------- | ------------------------------------------------------- | ------------- | -------- |
| :green_circle: | `highlightOnlySelected` | Whether to highlight only the selected rating item or not. | boolean | false | :x: |
| :purple_circle: | `halfFillMode` | Whether to half-fill the shape or the bounding box. | `svg` \| `box` | `svg` | :x: |
| :green_circle: | `itemStyles` | Custom shapes and colors. | ItemStyles | defaultStyles | :x: |
| :green_circle: | `spaceInside` | Responsive padding of each rating item. | `none` \| `small` \| `medium` \| `large` | `small` | :x: |
| :green_circle: | `spaceBetween` | Responsive gap between rating items. | `none` \| `small` \| `medium` \| `large` | `none` | :x: |
| :green_circle: | `radius` | Responsive radius of the bounding box. | `none` \| `small` \| `medium` \| `large` \| `full` | `none` | :x: |
| :green_circle: | `orientation` | Orientation of the rating items. | `horizontal` \| `vertical` | `horizontal` | :x: |
| :large_blue_circle: | `transition` | Transition to apply when hovering/selecting. | `none` \| `zoom` \| `colors` \| `opacity` \| `position` | `colors` | :x: |


### :open_umbrella: Accessibility

| :thinking: | Prop | Description | Type | Default | Required |
| ------------------- | --------------------- | ----------------------------------------------------------------------------------------------------------------- | -------- | -------------------------------------------------------------------------------- | -------- |
| :green_circle: | `invisibleLabel` | Accessible label of the rating group / image. | string | β€’ `Rating Selection` (radioGroup)
β€’ `Rated {value} on {items}` (readOnly) | :x: |
| :large_blue_circle: | `invisibleItemLabels` | Accessible labels of each rating item. | string[] | `Rate 1`, `Rate 2`... | :x: |
| :large_blue_circle: | `visibleLabelId` | DOM ID of the element used as rating group label. If set, takes precedence over `invisibleLabel`. | string | undefined | :x: |
| :large_blue_circle: | `visibleItemLabelIds` | DOM IDs of the elements used as labels for each rating item. If set, takes precedence over `invisibleItemLabels`. | string[] | undefined | :x: |
| :large_blue_circle: | `resetLabel` | Accessible label of the reset radio button. | string | `Reset Rating` | :x: |


## onChange

Type Definition

```ts
type RatingChange =
| React.Dispatch>
| ((selectedValue: number) => void | Promise)
```

### Basic

If your app doesn't require any custom logic/state to set the rating, you can simply pass the setter to `onChange`:

```js
function App() {
const [rating, setRating] = useState(0)

return
}
```

### Custom logic/state

If you need to perform actions while setting the rating (like calling an API) or you need to update a complex state, `onChange` accepts a callback whose only parameter equals to the selected rating:

```js
function App() {
const [state, setState] = useState({
review: '',
rating: 0 // Initial value
})

function handleChange(selectedValue) {
// 1. Logs the selected rating (1, 2, 3...)
console.log(selectedValue)

// 2. Do something with or without the value...

// 3. Update Rating UI
setState((prevState) => ({
...prevState,
rating: selectedValue
}))
}

return
}
```


## Behavior

### 1. Rating with reset - _Default_

By default, the user is able to reset the rating (from 1-5 to 0 and vice versa):

| Interaction | Reset action | Preview |
| ----------- | --------------------------------------------- | ------------------------------------------------------------------- |
| Mouse | By clicking again on the selected rating item | ![react-rating](https://i.ibb.co/pLPP1wM/ezgif-com-gif-maker-2.gif) |
| Keyboard | By navigating to an invisible reset radio | ![react-rating](https://i.ibb.co/3YWM7Fx/ezgif-com-gif-maker-1.gif) |

> :bulb: Don't like the default focus style? Check [here](#troubleshooting) how to customize it.

### 2. Rating without reset

There could be scenarios where you want to force the user to express a rating _(e.g. review page, post-service rating)_.

In such cases, set `isRequired` to **true**:

```jsx

```

![react-rating](https://i.ibb.co/BrtwWPX/ezgif-com-gif-maker-4.gif)

- It is not possible to reset by clicking again on the selected rating or by using the invisible radio.

- It is announced by screen readers that rating **is required**.

- If value equals to 0, it is announced by screen readers that rating **is invalid** .

> :bulb: Don't like the default focus style? Check [here](#troubleshooting) how to customize it.


## Styling

### Using included shapes

This package ships with six of the most common (open source) rating shapes that you can import and use:

| Import Name | Preview | Collection / Author |
| ----------------- | -------------------------------------------------------------------------------- | ------------------------------------------------------------ |
| `Star` | ![react-rating](https://i.ibb.co/0jS3F2P/Schermata-2022-09-29-alle-09-45-48.png) | [Feather](https://feathericons.com/) |
| `ThinStar` | ![react-rating](https://i.ibb.co/9hzfsmJ/Schermata-2022-10-01-alle-00-25-39.png) | [Raphael](https://github.com/dmitrybaranovskiy/raphael) |
| `RoundedStar` | ![react-rating](https://i.ibb.co/V9P422w/Schermata-2022-09-30-alle-23-47-02.png) | [Phosphor](https://phosphoricons.com/) |
| `ThinRoundedStar` | ![react-rating](https://i.ibb.co/tP3fRfz/Schermata-2022-09-30-alle-23-59-46.png) | [SVG Repo](https://www.svgrepo.com/svg/99804/star-favourite) |
| `StickerStar` | ![react-rating](https://i.ibb.co/C2sPq9X/Schermata-2022-10-01-alle-00-30-48.png) | [Raphael](https://www.svgrepo.com/svg/99804/star-favourite) |
| `Heart` | ![react-rating](https://i.ibb.co/7gvN66m/Schermata-2022-09-29-alle-10-26-24.png) | [Feather](https://feathericons.com/) |

```jsx
import { Rating, ThinStar } from '@smastrom/react-rating'

// Declare it outside your component so it doesn't get re-created
const myStyles = {
itemShapes: ThinStar,
activeFillColor: '#ffb700',
inactiveFillColor: '#fbf1a9'
}

function App() {
const [rating, setRating] = useState(0)

return (

)
}
```

Customizable properties


You can pass an object with the following properties to `itemStyles` prop:

```ts
type ItemStyles = {
itemShapes: JSX.Element | JSX.Element[]

itemStrokeWidth?: number
boxBorderWidth?: number

activeFillColor?: string | string[]
activeStrokeColor?: string | string[]
activeBoxColor?: string | string[]
activeBoxBorderColor?: string | string[]

inactiveFillColor?: string
inactiveStrokeColor?: string
inactiveBoxColor?: string
inactiveBoxBorderColor?: string
}
```

Besides `itemShapes`, **all the properties are optional**. If a property isn't defined, no classes nor CSS variables will be added to the SVG.

Just set the ones you need and that's it:

```jsx
const CustomStar = (

)

const myStyles = {
itemShapes: CustomStar,
activeFillColor: '#22C55E',
inactiveFillColor: '#BBF7D0'
}

function App() {
const [rating, setRating] = useState(4)

return (

)
}
```


### Using your own shapes

All you have to do is to open the SVG with a text editor, grab the inner shapes and delete any attribute except for geometric and transform ones.

If the SVG comes from quality sources such as any collection you can find on [IcΓ΄nes](https://icones.js.org/collection/all), all you have to do is to delete a couple of fill and stroke attributes (if any):

```html

```

Then define a JSX element to render the shapes. Rating will take care of rendering a brand-new, responsive SVG for you:

```jsx
const CustomStar = (

)

const myStyles = {
itemShapes: CustomStar,
itemStrokeWidth: 2,
activeFillColor: 'LightSeaGreen',
activeStrokeColor: '#99F6E4',
inactiveFillColor: '#99F6E4',
inactiveStrokeColor: 'LightSeaGreen'
}

function App() {
const [rating, setRating] = useState(4)

return (

)
}
```


Customizable properties


You can pass an object with the following properties to `itemStyles` prop:

```ts
type ItemStyles = {
itemShapes: JSX.Element | JSX.Element[]

itemStrokeWidth?: number
boxBorderWidth?: number

activeFillColor?: string | string[]
activeStrokeColor?: string | string[]
activeBoxColor?: string | string[]
activeBoxBorderColor?: string | string[]

inactiveFillColor?: string
inactiveStrokeColor?: string
inactiveBoxColor?: string
inactiveBoxBorderColor?: string
}
```

Besides `itemShapes`, **all the properties are optional**. If a property isn't defined, no classes nor CSS variables will be added to the SVG.

Just set the ones you need and that's it:

```jsx
const CustomStar = (

)

const myStyles = {
itemShapes: CustomStar,
activeFillColor: '#22C55E',
inactiveFillColor: '#BBF7D0'
}

function App() {
const [rating, setRating] = useState(4)

return (

)
}
```

Default styles

```js
import { Star } from '@smastrom/react-rating'

const defaultItemStyles = {
itemShapes: Star,
itemStrokeWidth: 2,
activeFillColor: '#ffb23f',
activeStrokeColor: '#e17b21',
inactiveFillColor: '#fff7ed',
inactiveStrokeColor: '#e17b21'
}
```

How itemStrokeWidth works

The stroke width is expressed in _viewBox user coordinate's unit size_ and **not in pixels**.

Depending on the vector nodes provided you may have to input and try different values in order to reach the desired stroke width.

It is responsive by nature, so expect it to increase/decrease when resizing the container.

Color values

You can pass any valid CSS color string such as `aliceblue`, `#FFF332`, `rgba(0, 0, 0, 0)` or `hsl(69, 22, 200)`.

TypeScript

```tsx
import type { ItemStyles } from '@smastrom/react-rating'

const Star = (

)

const myStyles: ItemStyles = {
itemShapes: Star,
activeFillColor: 'green',
inactiveFillColor: 'gray'
}
```


Quick guide for complex or messy SVGs

1. Keep only the groups and the inner shapes of the svg: `g`, `path`, `circle`, `rect`, `polygon`, `ellipse`, `polyline` or `line` and delete any other node (e.g. ``).

2. If a group is present, check if it has the `transform` attribute set. If the attribute is not set, keep the inner shapes and delete the `g` node.

3. Delete any attribute **except** geometric, draw and transform ones from any group and shape.

4. If present, delete any empty node like `` or ``.


### Per-active-item styling

If you wish to style each rating item, you can optionally pass an array of JSX elements to `itemShapes` and an array of valid CSS colors to any **active** property:

![react-rating](https://i.ibb.co/QXsDp8B/Schermata-2022-06-30-alle-01-30-51.png)

```jsx
const SadFace = (

)

const SmilingFace = (

)

const myStyles = {
itemShapes: [SadFace, SmilingFace],
activeFillColor: ['#da1600', '#61bb00'],
inactiveFillColor: '#a8a8a8'
}

function App() {
const [rating, setRating] = useState(0)

return (

)
}
```

![react-rating](https://s8.gifyu.com/images/in_AdobeExpress.gif)

```jsx
const Star = (

)

const myStyles = {
itemShapes: Star,
boxBorderWidth: 3,

activeFillColor: ['#FEE2E2', '#FFEDD5', '#FEF9C3', '#ECFCCB', '#D1FAE5'],
activeBoxColor: ['#da1600', '#db711a', '#dcb000', '#61bb00', '#009664'],
activeBoxBorderColor: ['#c41400', '#d05e00', '#cca300', '#498d00', '#00724c'],

inactiveFillColor: 'white',
inactiveBoxColor: '#dddddd',
inactiveBoxBorderColor: '#a8a8a8'
}

function App() {
const [rating, setRating] = useState(0)

return (

)
}
```


### Half-fill and float values

If `readOnly` is set to **true**, the `value` prop accepts a float:

```jsx

```

The component will try to round it to the nearest half integer:

```js
3.2 = 3
3.26 = 3.5
3.62 = 3.5
3.75 = 4
```

> :warning: The value will only be rounded "internally" for graphical purposes. The accessible label will always display the value provided.

If necessary, the SVG will be half-filled by default (`halfFillMode = 'svg'`):

![react-rating](https://i.ibb.co/V2kJ317/Screenshot-2022-11-13-alle-12-07-51.png)

All the boxes will have the same background color (inactiveBoxColor) and `activeBoxColor` will have no effect.

You can switch between `svg` and `box`:

```jsx

```

![react-rating](https://i.ibb.co/sKpybbV/Schermata-2022-06-01-alle-23-43-29.png)

In this case instead, all the shapes will have the same fill color (inactiveFillColor) and `activeFillColor` will have no effect.

If you don't want the half-fill feature, simply pass an integer to `value`.

> :warning: If `highlightOnlySelected` is set to **true**, no half-fill will take place.


## Accessibility

### Mouse/keyboard callbacks

React Rating leverages `aria radiogroup` role instead of native HTML radio buttons in order to improve keyboard-users experience and extend capabilities (e.g. API calls) by keeping callbacks consistent between keyboard and mouse.

In **React Rating**:

- Rating must be confirmed with `Enter/Space` keys and cannot be set directly with arrows like native radios.

- `onChange` is called on both `Enter/Space` keys and click.

- `onHoverChange` is called on `← β†’ ↑ ↓` navigation, mouse hovering, focus-from / blur-to a _non-react-rating_ element.

### Disabled state

It is always announced by screen readers instead of being completely hidden.

### Labels

React Rating ships with default accessible labels computed from your `items` value. In order to customize them or to switch to visible ones check the multiple examples on the [demo website](https://reactrating.netlify.app).


## Troubleshooting

### I can see the nodes returned from rendering, but no styles have been applied.

Check that you are importing the CSS as displayed in the [Basic usage](#basic-usage) section.

### I keep getting the error: "itemShapes is not a valid JSX element".

Check that you are passing a JSX element and not a functional component:

:white_check_mark: **Correct**

```jsx
const Star =
```

:x: **Incorrect**

```jsx
const Star = () =>
```

### I passed an array of shapes but the stroke width looks different for each item.

When passing different shapes for each rating item, this package forces you to use icons from the same collection to keep design consistency. Be sure you are doing that.

You can find clean, SVG collections at [IcΓ΄nes](https://icones.js.org/collection/all).

### I don't like the default focus ring styles. How can I style them?

It is possible to style them via CSS by targeting the following selectors:

**Rating items**

```css
.rr--box:focus-visible .rr--svg {
/* Your styles */
}
```

**Reset**

```css
.rr--focus-reset {
/* Your styles */
}
```

Defaults

```css
.rr--focus-reset {
outline: 6px double #0079ff;
}

.rr--box:focus-visible .rr--svg {
outline: 6px double #0079ff;
}
```


## License

MIT