Saltar al contenido
ssntl/ui

Swatch

Un selector de variantes con radios nombrados y objetivos de 44px, debajo de la vista previa que cambian.

Finish: Blue

Instalación

$ pnpm dlx shadcn@latest add @ssntl/swatch

Uso

tsx
import { Swatch, SwatchGroup } from "@/components/ui/swatch"

<SwatchGroup aria-label="Color">
  <Swatch name="color" value="azul" color="var(--blue-600)" aria-label="Azul" defaultChecked />
  <Swatch name="color" value="negro" color="var(--neutral-950)" aria-label="Negro" />
</SwatchGroup>
  • Visibilidad al tocar: pon los swatches debajo de la vista previa que cambian, así el dedo nunca tapa el resultado.
  • Cada swatch es un radio con nombre (aria-label): el color solo no significa nada para un lector de pantalla. Muestra también el nombre elegido en texto.
  • Un objetivo de 44px alrededor de un punto de 32px; el elegido lleva un anillo de 2px con separación, no solo un cambio de borde.
  • Pasa un color del sistema (var(--blue-600)) o el color real del producto; va en un style en línea porque es un dato.

Código

components/ui/swatch.tsx
import * as React from "react"

import { cn } from "@/lib/utils"

// A variant picker: color, finish, pattern. Each swatch is a native radio
// with a 44px target around a 32px dot, named with aria-label because the
// color alone means nothing to a screen reader. Touch visibility: put the
// group below the preview it changes, so the finger never covers the result.
function SwatchGroup({
  className,
  ...props
}: React.ComponentProps<"fieldset">) {
  return (
    <fieldset
      data-slot="swatch-group"
      className={cn("flex min-w-0 flex-wrap items-center gap-1", className)}
      {...props}
    />
  )
}

function Swatch({
  color,
  className,
  style,
  ...props
}: Omit<React.ComponentProps<"input">, "type" | "color"> & {
  /** Any CSS color; it is data, so it goes in an inline style. */
  color: string
  "aria-label": string
}) {
  return (
    <label
      data-slot="swatch"
      className="grid size-11 shrink-0 cursor-pointer place-items-center rounded-full has-disabled:cursor-not-allowed has-disabled:opacity-40"
    >
      <input
        type="radio"
        style={{ ...style, backgroundColor: color }}
        className={cn(
          "size-8 cursor-pointer appearance-none rounded-full outline -outline-offset-1 outline-black/10 dark:outline-white/10",
          "ring-offset-2 ring-offset-card transition-[box-shadow] duration-(--duration-fast) ease-out checked:ring-2 checked:ring-foreground",
          "focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-ring",
          className
        )}
        {...props}
      />
    </label>
  )
}

export { Swatch, SwatchGroup }