HextaUI

ベータVercel の UI SDK 0.1.0-beta.1 上に構築。API は変わる可能性があります。

Rating

Vercel の UI SDK 上の星評価。全星・半星、ホバー時のプレビュー、読み取り専用の平均値、フォーム対応を備えます。

依存関係
  • @tabler/icons-react
  • @ui-sdk/core
  • @ui-sdk/react
  • class-variance-authority
"use client"

import * as React from "react"

import { Rating, RatingItem } from "@/components/ui/ui-sdk-rating"

const labels = ["Terrible", "Poor", "Okay", "Good", "Great"]

function UiSdkRatingDemo() {
  const [value, setValue] = React.useState<number | null>(4)

  return (
    <div className="flex flex-col items-center gap-2">
      <Rating
        aria-label="How was your stay?"
        size="lg"
        value={value}
        onValueChange={setValue}
        getValueLabel={(score, max) =>
          labels[score - 1] + ", " + score + " of " + max
        }
      >
        <RatingItem />
      </Rating>
      <p
        aria-hidden="true"
        className="min-h-5 text-sm text-muted-foreground tabular-nums"
      >
        {value ? labels[value - 1] : "Not rated yet"}
      </p>
    </div>
  )
}

export { UiSdkRatingDemo }
pnpm dlx shadcn@latest add https://hextaui.com/r/ui-sdk-rating.json

コンポーネント、HextaUIのテーマトークン、依存するHextaUIコンポーネントを追加します。

UI SDK は ui-sdk というカスケードレイヤーに少しだけ独自の CSS を追加します。自分のクラスが優先されるよう、グローバルスタイルシートの先頭、Tailwind の import より上でこのレイヤーを宣言してください。

@layer ui-sdk;

@import "tailwindcss";
import { Rating, RatingItem } from "@/components/ui/ui-sdk-rating"
<Rating aria-label="Rating" defaultValue={3}>
  <RatingItem />
</Rating>

Rating は子を星ごとに 1 回描画するので、中に RatingItem を 1 つ置きます。各星は空の星に --rating-fill で切り抜いた塗りの星を重ねるため、半星や 4.3 のような平均も正確に表示されます。

Rating
└── RatingItem

step={0.5} では星の半分ごとにラジオがあり、矢印キーで半星ずつ動きます。

import { Rating, RatingItem } from "@/components/ui/ui-sdk-rating"

function UiSdkRatingHalf() {
  return (
    <Rating aria-label="Rating" step={0.5} defaultValue={3.5}>
      <RatingItem />
    </Rating>
  )
}

export { UiSdkRatingHalf }

読み取り専用の評価は getValueLabel で名前が付く 1 枚の画像で、平均値のような任意のスコアを表示します。

"use client"

import { Rating, RatingItem } from "@/components/ui/ui-sdk-rating"

function UiSdkRatingReadOnly() {
  return (
    <div className="flex items-center gap-2 text-sm">
      <Rating
        readOnly
        size="sm"
        value={4.3}
        getValueLabel={(value, max) =>
          "Rated " + value + " out of " + max + " stars"
        }
      >
        <RatingItem />
      </Rating>
      <span className="font-medium tabular-nums">4.3</span>
      <span className="text-muted-foreground">(1,284 reviews)</span>
    </div>
  )
}

export { UiSdkRatingReadOnly }

size は sm、default、lg のいずれかです。

import { Rating, RatingItem } from "@/components/ui/ui-sdk-rating"

function UiSdkRatingSizes() {
  return (
    <div className="flex flex-col items-center gap-4">
      <Rating aria-label="Small rating" size="sm" defaultValue={3}>
        <RatingItem />
      </Rating>
      <Rating aria-label="Default rating" defaultValue={3}>
        <RatingItem />
      </Rating>
      <Rating aria-label="Large rating" size="lg" defaultValue={3}>
        <RatingItem />
      </Rating>
    </div>
  )
}

export { UiSdkRatingSizes }

deselectable を指定すると、選択中の星をもう一度押してスコアを消せます。

import { Rating, RatingItem } from "@/components/ui/ui-sdk-rating"

function UiSdkRatingDeselectable() {
  return (
    <Rating aria-label="Rating" deselectable defaultValue={2}>
      <RatingItem />
    </Rating>
  )
}

export { UiSdkRatingDeselectable }

value と onValueChange を渡します。null はまだスコアがないことを表します。createRatingController() のコントローラーでどこからでもスコアを設定できます。

"use client"

import * as React from "react"

import { Button } from "@/components/ui/button"
import {
  createRatingController,
  Rating,
  RatingItem,
} from "@/components/ui/ui-sdk-rating"

function UiSdkRatingControlled() {
  const [rating] = React.useState(createRatingController)
  const [value, setValue] = React.useState<number | null>(null)

  return (
    <div className="flex flex-col items-center gap-3">
      <Rating
        aria-label="Rating"
        controller={rating}
        value={value}
        onValueChange={setValue}
      >
        <RatingItem />
      </Rating>
      <div className="flex gap-2">
        <Button variant="outline" size="sm" onClick={() => rating.setValue(5)}>
          Rate 5
        </Button>
        <Button
          variant="ghost"
          size="sm"
          disabled={value === null}
          onClick={() => setValue(null)}
        >
          Clear
        </Button>
      </div>
      <p className="text-sm text-muted-foreground tabular-nums">
        {value === null ? "No score" : "Score: " + value}
      </p>
    </div>
  )
}

export { UiSdkRatingControlled }

name を付けるとスコアがフォームと一緒に送信されます。required はスコアが入るまで、ブラウザー標準のメッセージで送信を止めます。

"use client"

import * as React from "react"

import { Button } from "@/components/ui/button"
import { Rating, RatingItem } from "@/components/ui/ui-sdk-rating"

function UiSdkRatingForm() {
  const [submitted, setSubmitted] = React.useState<string | null>(null)

  return (
    <form
      className="flex flex-col items-start gap-3"
      onSubmit={(event) => {
        event.preventDefault()
        setSubmitted(String(new FormData(event.currentTarget).get("score")))
      }}
      onReset={() => setSubmitted(null)}
    >
      <div className="flex flex-col gap-1.5">
        <span id="rating-form-label" className="text-sm font-medium">
          Rate this answer
        </span>
        <Rating aria-labelledby="rating-form-label" name="score" required>
          <RatingItem />
        </Rating>
      </div>
      <div className="flex gap-2">
        <Button type="submit" size="sm">
          Submit
        </Button>
        <Button type="reset" variant="outline" size="sm">
          Reset
        </Button>
      </div>
      <p role="status" className="text-sm text-muted-foreground">
        {submitted ? "Submitted score=" + submitted : "Nothing submitted yet."}
      </p>
    </form>
  )
}

export { UiSdkRatingForm }

独自のアイコンを RatingItem の子として渡し、data-filled で切り替えます。tone で色を設定します。

import { IconHeart, IconHeartFilled } from "@tabler/icons-react"

import { Rating, RatingItem } from "@/components/ui/ui-sdk-rating"

function UiSdkRatingCustomIcon() {
  return (
    <Rating
      aria-label="How much did you like it?"
      defaultValue={3}
      tone="destructive"
    >
      <RatingItem>
        <IconHeart
          aria-hidden="true"
          className="size-6 group-data-[filled]/rating-item:hidden group-data-[partial]/rating-item:hidden"
        />
        <IconHeartFilled
          aria-hidden="true"
          className="hidden size-6 group-data-[filled]/rating-item:block group-data-[partial]/rating-item:block"
        />
      </RatingItem>
    </Rating>
  )
}

export { UiSdkRatingCustomIcon }

無効な評価はフォーカスも変更もできず、何も送信しません。

import { Rating, RatingItem } from "@/components/ui/ui-sdk-rating"

function UiSdkRatingDisabled() {
  return (
    <Rating aria-label="Rating" disabled defaultValue={3}>
      <RatingItem />
    </Rating>
  )
}

export { UiSdkRatingDisabled }

星は右から並び、半星は右端から塗られます。

import { Rating, RatingItem } from "@/components/ui/ui-sdk-rating"

function UiSdkRatingRtl() {
  return (
    <div dir="rtl">
      <Rating aria-label="التقييم" step={0.5} defaultValue={2.5}>
        <RatingItem />
      </Rating>
    </div>
  )
}

export { UiSdkRatingRtl }
キーアクション
Tab評価に移動し、選択中の星か最初の星にフォーカスします。
←→↑↓次または前のスコアに移動して選択します。loop を指定しない限り端で止まります。
Spaceフォーカス中の星を選択します。選択済みで解除可能な評価の場合は解除します。
Enterフォームを送信します。

評価はタブ移動先が 1 つのラジオグループです。各星は選べるスコアごとにラジオを持ち、getValueLabel で名前が付くので、「最高、5 点中 5 点」のように普段の言葉で名付けます。星のアイコンはスクリーンリーダーから隠されます。読み取り専用の評価は 1 枚の画像になります。

モーションが許可されている場合のみ、星はホバーで大きくなり、押すと縮みます。タッチ画面では各星に余白が追加され、強制カラーモードでは塗られた星が文字色、空の星がシステムのグレーになります。

各パーツは同名の UI SDK パーツをラップし、render やパーツの状態を受け取る関数にもできる className を含むすべての props を受け付けます。

プロパティ型デフォルト
value
number | null–
defaultValue
number | null–
onValueChange
(value: number | null, details) => void–
max
number5
step
number1
size
"sm" | "default" | "lg""default"
tone
"default" | "neutral" | "destructive""default"
deselectable
booleanfalse
readOnly
booleanfalse
disabled
booleanfalse
required
booleanfalse
name
string–
loop
booleanfalse
getValueLabel各スコアのラジオと、読み取り専用の評価に名前を付けます。
(value: number, max: number) => string–
controllercreateRatingController() から作成します。
RatingController–
属性説明
data-slot="rating"CSS で評価を指定します。
data-sizeサイズ:sm、default、lg。
data-toneトーン:default、neutral、destructive。
data-readonly読み取り専用のときに付与されます。
data-disabled無効のときに存在します。
--rating-color塗られた星の色。tone で設定します。
プロパティ型デフォルト
children星のアイコン。既定では --rating-fill に応じて塗られる Tabler の星です。
ReactNode–
render
ReactElement | (props, state) => ReactElement–
属性説明
data-slot="rating-item"CSS で星を指定します。
data-filled表示中のスコアが完全に覆う星に付きます。
data-partial表示中のスコアが一部を覆う星に付きます。
data-checkedスコアが終わる星に付きます。
data-highlightedポインターがプレビューしているスコアまでの星に付きます。
--rating-fill表示中のスコアが星をどれだけ覆うか(0〜1)。