HextaUI

BêtaConstruit sur l’UI SDK 0.1.0-beta.2 de Vercel. Son API peut changer.

Segmented control

Un contrôle segmenté sur l’UI SDK de Vercel avec un indicateur qui glisse, trois tailles, des éléments à icône seule, la pleine largeur, une mise en page verticale et le clavier d’un groupe radio.

Dépend de
  • @ui-sdk/core
  • @ui-sdk/react
import { IconLayoutGrid, IconLayoutKanban, IconList } from "@tabler/icons-react"

import {
  SegmentedControl,
  SegmentedControlItem,
} from "@/components/ui/ui-sdk-segmented-control"

function UiSdkSegmentedControlDemo() {
  return (
    <SegmentedControl aria-label="View" defaultValue="list">
      <SegmentedControlItem value="list">
        <IconList aria-hidden="true" />
        List
      </SegmentedControlItem>
      <SegmentedControlItem value="grid">
        <IconLayoutGrid aria-hidden="true" />
        Grid
      </SegmentedControlItem>
      <SegmentedControlItem value="board">
        <IconLayoutKanban aria-hidden="true" />
        Board
      </SegmentedControlItem>
    </SegmentedControl>
  )
}

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

Ajoute le composant, les tokens de thème HextaUI et les composants HextaUI dont il dépend.

UI SDK ajoute un peu de CSS dans une couche de cascade nommée ui-sdk. Déclarez cette couche en haut de votre feuille de style globale, au-dessus de l’import Tailwind, pour que vos classes l’emportent.

@layer ui-sdk;

@import "tailwindcss";
import {
  SegmentedControl,
  SegmentedControlItem,
} from "@/components/ui/ui-sdk-segmented-control"
<SegmentedControl aria-label="View" defaultValue="list">
  <SegmentedControlItem value="list">List</SegmentedControlItem>
  <SegmentedControlItem value="grid">Grid</SegmentedControlItem>
</SegmentedControl>

Un contrôle segmenté bascule entre quelques vues ou réglages. Il fonctionne comme un groupe radio, avec une valeur et un seul arrêt de tabulation, et l’indicateur derrière l’élément coché glisse vers le nouveau choix. Nommez-le avec aria-label ou aria-labelledby. UI SDK le marque comme expérimental en 0.1.0-beta.2.

SegmentedControl
├── SegmentedControlItem
└── SegmentedControlItem

Le contrôle dessine son propre indicateur, vous n’ajoutez que les éléments.

size vaut sm, default ou lg. Les éléments grandissent sur écran tactile.

import {
  SegmentedControl,
  SegmentedControlItem,
} from "@/components/ui/ui-sdk-segmented-control"

const sizes = ["sm", "default", "lg"] as const

function UiSdkSegmentedControlSizes() {
  return (
    <div className="flex flex-col items-center gap-4">
      {sizes.map((size) => (
        <SegmentedControl
          key={size}
          aria-label={`Effort, ${size}`}
          size={size}
          defaultValue="balanced"
        >
          <SegmentedControlItem value="fast">Fast</SegmentedControlItem>
          <SegmentedControlItem value="balanced">Balanced</SegmentedControlItem>
          <SegmentedControlItem value="thorough">Thorough</SegmentedControlItem>
        </SegmentedControl>
      ))}
    </div>
  )
}

export { UiSdkSegmentedControlSizes }

Un élément qui ne contient qu’une icône devient carré. Nommez-le avec aria-label.

import {
  IconAlignCenter,
  IconAlignJustified,
  IconAlignLeft,
  IconAlignRight,
} from "@tabler/icons-react"

import {
  SegmentedControl,
  SegmentedControlItem,
} from "@/components/ui/ui-sdk-segmented-control"

function UiSdkSegmentedControlIcons() {
  return (
    <SegmentedControl aria-label="Text alignment" defaultValue="left">
      <SegmentedControlItem value="left" aria-label="Align left">
        <IconAlignLeft aria-hidden="true" />
      </SegmentedControlItem>
      <SegmentedControlItem value="center" aria-label="Align center">
        <IconAlignCenter aria-hidden="true" />
      </SegmentedControlItem>
      <SegmentedControlItem value="right" aria-label="Align right">
        <IconAlignRight aria-hidden="true" />
      </SegmentedControlItem>
      <SegmentedControlItem value="justify" aria-label="Justify">
        <IconAlignJustified aria-hidden="true" />
      </SegmentedControlItem>
    </SegmentedControl>
  )
}

export { UiSdkSegmentedControlIcons }

Donnez w-full au contrôle et les éléments se partagent sa largeur.

import {
  SegmentedControl,
  SegmentedControlItem,
} from "@/components/ui/ui-sdk-segmented-control"

function UiSdkSegmentedControlFullWidth() {
  return (
    <div className="flex w-full max-w-sm flex-col gap-2">
      <p id="billing-period" className="m-0 text-sm font-medium">
        Billing period
      </p>
      <SegmentedControl
        aria-labelledby="billing-period"
        defaultValue="yearly"
        className="w-full"
      >
        <SegmentedControlItem value="monthly">Monthly</SegmentedControlItem>
        <SegmentedControlItem value="yearly">Yearly</SegmentedControlItem>
      </SegmentedControl>
    </div>
  )
}

export { UiSdkSegmentedControlFullWidth }

orientation="vertical" empile les éléments. Les flèches des deux axes passent toujours de l’un à l’autre.

import {
  IconLayoutAlignBottom,
  IconLayoutAlignMiddle,
  IconLayoutAlignTop,
} from "@tabler/icons-react"

import {
  SegmentedControl,
  SegmentedControlItem,
} from "@/components/ui/ui-sdk-segmented-control"

function UiSdkSegmentedControlVertical() {
  return (
    <SegmentedControl
      aria-label="Vertical alignment"
      orientation="vertical"
      defaultValue="middle"
    >
      <SegmentedControlItem value="top">
        <IconLayoutAlignTop aria-hidden="true" />
        Top
      </SegmentedControlItem>
      <SegmentedControlItem value="middle">
        <IconLayoutAlignMiddle aria-hidden="true" />
        Middle
      </SegmentedControlItem>
      <SegmentedControlItem value="bottom">
        <IconLayoutAlignBottom aria-hidden="true" />
        Bottom
      </SegmentedControlItem>
    </SegmentedControl>
  )
}

export { UiSdkSegmentedControlVertical }

disabled sur le contrôle le désactive entièrement et n’envoie rien. Sur un élément, les flèches le sautent.

import {
  SegmentedControl,
  SegmentedControlItem,
} from "@/components/ui/ui-sdk-segmented-control"

function UiSdkSegmentedControlDisabled() {
  return (
    <div className="flex flex-col items-center gap-4">
      <SegmentedControl aria-label="Theme" defaultValue="system" disabled>
        <SegmentedControlItem value="light">Light</SegmentedControlItem>
        <SegmentedControlItem value="dark">Dark</SegmentedControlItem>
        <SegmentedControlItem value="system">System</SegmentedControlItem>
      </SegmentedControl>
      <SegmentedControl aria-label="Model" defaultValue="mini">
        <SegmentedControlItem value="mini">Mini</SegmentedControlItem>
        <SegmentedControlItem value="standard">Standard</SegmentedControlItem>
        <SegmentedControlItem value="pro" disabled>
          Pro
        </SegmentedControlItem>
      </SegmentedControl>
    </div>
  )
}

export { UiSdkSegmentedControlDisabled }

Passez value et mettez-le à jour dans onValueChange. Un contrôleur issu de createSegmentedControlController() coche un élément depuis n’importe où.

"use client"

import * as React from "react"

import { Button } from "@/components/ui/button"
import {
  SegmentedControl,
  SegmentedControlItem,
  createSegmentedControlController,
} from "@/components/ui/ui-sdk-segmented-control"

type Range = "day" | "week" | "month"

function UiSdkSegmentedControlControlled() {
  const [controller] = React.useState(() =>
    createSegmentedControlController<Range>()
  )
  const [range, setRange] = React.useState<Range | null>("week")

  return (
    <div className="flex flex-col items-center gap-3">
      <SegmentedControl<Range>
        aria-label="Range"
        value={range}
        onValueChange={(value) => setRange(value)}
        controller={controller}
      >
        <SegmentedControlItem value="day">Day</SegmentedControlItem>
        <SegmentedControlItem value="week">Week</SegmentedControlItem>
        <SegmentedControlItem value="month">Month</SegmentedControlItem>
      </SegmentedControl>
      <div className="flex items-center gap-2">
        <Button
          size="sm"
          variant="outline"
          onClick={() => controller.setValue("month")}
        >
          Show month
        </Button>
        <p className="m-0 text-xs text-muted-foreground">
          {`Showing ${range ?? "nothing"}`}
        </p>
      </div>
    </div>
  )
}

export { UiSdkSegmentedControlControlled }

Chaque élément contient un bouton radio natif caché, donc name envoie la valeur cochée, required attend un choix, et la réinitialisation revient à la valeur par défaut.

"use client"

import * as React from "react"

import { Button } from "@/components/ui/button"
import {
  SegmentedControl,
  SegmentedControlItem,
} from "@/components/ui/ui-sdk-segmented-control"

function UiSdkSegmentedControlForm() {
  const [saved, setSaved] = React.useState<string | null>(null)

  return (
    <form
      className="flex flex-col items-start gap-4"
      onSubmit={(event) => {
        event.preventDefault()
        const data = new FormData(event.currentTarget)
        setSaved(`theme=${data.get("theme")}, density=${data.get("density")}`)
      }}
    >
      <div className="flex flex-col gap-2">
        <p id="form-theme" className="m-0 text-sm font-medium">
          Theme
        </p>
        <SegmentedControl
          aria-labelledby="form-theme"
          name="theme"
          defaultValue="system"
          required
        >
          <SegmentedControlItem value="light">Light</SegmentedControlItem>
          <SegmentedControlItem value="dark">Dark</SegmentedControlItem>
          <SegmentedControlItem value="system">System</SegmentedControlItem>
        </SegmentedControl>
      </div>
      <div className="flex flex-col gap-2">
        <p id="form-density" className="m-0 text-sm font-medium">
          Density
        </p>
        <SegmentedControl
          aria-labelledby="form-density"
          name="density"
          defaultValue="comfortable"
        >
          <SegmentedControlItem value="compact">Compact</SegmentedControlItem>
          <SegmentedControlItem value="comfortable">
            Comfortable
          </SegmentedControlItem>
        </SegmentedControl>
      </div>
      <div className="flex items-center gap-3">
        <Button type="submit" size="sm">
          Save
        </Button>
        <Button type="reset" size="sm" variant="ghost">
          Reset
        </Button>
      </div>
      <p className="m-0 text-sm text-muted-foreground">
        {saved ? `Saved ${saved}` : "Nothing saved yet."}
      </p>
    </form>
  )
}

export { UiSdkSegmentedControlForm }

Un contrôle en lecture seule garde sa valeur et est toujours envoyé. Les flèches déplacent le focus sans rien cocher.

import {
  SegmentedControl,
  SegmentedControlItem,
} from "@/components/ui/ui-sdk-segmented-control"

function UiSdkSegmentedControlReadOnly() {
  return (
    <SegmentedControl aria-label="Plan" defaultValue="team" readOnly>
      <SegmentedControlItem value="solo">Solo</SegmentedControlItem>
      <SegmentedControlItem value="team">Team</SegmentedControlItem>
    </SegmentedControl>
  )
}

export { UiSdkSegmentedControlReadOnly }

Les éléments partent de la droite, et l’indicateur les suit.

import {
  SegmentedControl,
  SegmentedControlItem,
} from "@/components/ui/ui-sdk-segmented-control"

function UiSdkSegmentedControlRtl() {
  return (
    <div dir="rtl" lang="ar">
      <SegmentedControl aria-label="العرض" defaultValue="list">
        <SegmentedControlItem value="list">قائمة</SegmentedControlItem>
        <SegmentedControlItem value="grid">شبكة</SegmentedControlItem>
        <SegmentedControlItem value="board">لوحة</SegmentedControlItem>
      </SegmentedControl>
    </div>
  )
}

export { UiSdkSegmentedControlRtl }
ToucheAction
TabEntre dans le contrôle, sur l’élément coché ou le premier actif.
←→↑↓Passent à l’élément suivant ou précédent et le cochent, en sautant les désactivés. Ils bouclent sauf si loop vaut false.
SpaceCoche l’élément ciblé.
EnterEnvoie son formulaire.

Le contrôle est un groupe radio et chaque élément un bouton radio, donc les lecteurs d’écran annoncent le choix et sa position. L’indicateur est décoratif et ignoré par les lecteurs d’écran.

L’indicateur ne glisse qu’une fois la page interactive, il ne s’anime donc jamais au chargement, et pas du tout quand les animations sont réduites. En couleurs forcées, l’élément coché garde une bordure visible.

Chaque partie enveloppe la partie UI SDK du même nom et accepte toutes ses props, y compris render et un className qui peut être une fonction de son état.

PropTypePar défaut
value
T | null–
defaultValue
T | null–
onValueChange
(value: T | null, details) => void–
size
"sm" | "default" | "lg""default"
orientation
"horizontal" | "vertical""horizontal"
loop
booleantrue
disabled
booleanfalse
readOnly
booleanfalse
required
booleanfalse
name
string–
form
string–
controllerIssu de createSegmentedControlController().
SegmentedControlController–
render
ReactElement | (props, state) => ReactElement–
AttributDescription
data-slot="segmented-control"Pour cibler le contrôle segmenté en CSS.
data-sizeLa taille : sm, default ou lg.
data-orientationhorizontal ou vertical.
data-disabledPrésent lorsque l’élément est désactivé.
data-readonlyPrésent en lecture seule.
data-requiredPrésent lorsqu’elle est requise.
PropTypePar défaut
value
string–
disabled
booleanfalse
render
ReactElement | (props, state) => ReactElement–
AttributDescription
data-slot="segmented-control-item"Pour cibler un élément en CSS.
data-checkedPrésent sur l’élément coché.
data-uncheckedPrésent sur les autres éléments.
data-icon-onlyPrésent quand l’élément ne contient qu’une icône.
data-disabledPrésent lorsque l’élément est désactivé.