Saltar al contenido
VEXA
Acciones

IconButton

Apto para servidor

A square, icon-only action that requires an accessible name.

Vista previa en vivo

Renderizado desde el código fuente instalado de VEXA — los mismos archivos que recibes.

Escritorio

Playground

Ajusta las props y copia el JSX equivalente.

Playground

icon-button.tsx
<IconButton aria-label="Action">
  <PlusIcon />
</IconButton>

Instalación

Añade el código del componente a tu proyecto. Las dependencias del registry se resuelven automáticamente.

npx shadcn@latest add https://vexa.valfiguer.com/r/icon-button.json

Se instala en components/vexa/icon-button.tsx

Dependencias npm

  • @radix-ui/react-slot
  • class-variance-authority

Dependencias del registry

  • @vexa/utils

Otros items de VEXA que necesita este componente. Instálalos primero — la CLI los resuelve automáticamente.

Código

Cópialo a su ruta destino, o instálalo con el comando de arriba.

components/vexa/icon-button.tsx
import type * as React from "react";
import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority";

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

export const iconButtonVariants = cva(
  [
    "inline-flex select-none items-center justify-center",
    "rounded-vexa-md text-sm font-medium",
    "transition-[color,background-color,border-color,box-shadow]",
    "duration-[var(--vexa-duration-fast)] ease-[var(--vexa-ease-standard)]",
    "outline-none focus-visible:ring-2 focus-visible:ring-vexa-ring",
    "focus-visible:ring-offset-2 focus-visible:ring-offset-vexa-background",
    "disabled:pointer-events-none disabled:opacity-50",
    "aria-[busy=true]:pointer-events-none aria-[busy=true]:cursor-progress",
    "[&_svg]:pointer-events-none [&_svg]:shrink-0",
  ],
  {
    variants: {
      variant: {
        primary:
          "bg-vexa-primary text-vexa-primary-foreground hover:bg-vexa-primary/90 active:bg-vexa-primary/95",
        secondary:
          "border border-vexa-border bg-vexa-secondary text-vexa-secondary-foreground hover:bg-vexa-accent active:bg-vexa-accent",
        outline:
          "border border-vexa-input bg-transparent text-vexa-foreground hover:bg-vexa-accent hover:text-vexa-accent-foreground active:bg-vexa-accent",
        ghost:
          "bg-transparent text-vexa-foreground hover:bg-vexa-accent hover:text-vexa-accent-foreground active:bg-vexa-accent",
        danger:
          "bg-vexa-danger text-vexa-danger-foreground hover:bg-vexa-danger/90 active:bg-vexa-danger/95",
      },
      size: {
        sm: "size-8 [&_svg]:size-4",
        md: "size-10 [&_svg]:size-5",
        lg: "size-12 [&_svg]:size-6",
      },
    },
    defaultVariants: {
      variant: "ghost",
      size: "md",
    },
  },
);

/**
 * An IconButton MUST have an accessible name. This type enforces at least one
 * of `aria-label` or `aria-labelledby` at compile time, since the button has no
 * visible text label.
 */
type AccessibleName =
  | { "aria-label": string; "aria-labelledby"?: never }
  | { "aria-labelledby": string; "aria-label"?: never };

export type IconButtonProps = Omit<
  React.ComponentProps<"button">,
  "aria-label" | "aria-labelledby"
> &
  VariantProps<typeof iconButtonVariants> & {
    asChild?: boolean;
  } & AccessibleName;

/**
 * IconButton — a square, icon-only action. Requires an accessible name.
 *
 * RSC-safe. Square touch targets: `md` and `lg` meet the 44×44 CSS-pixel
 * guidance; use `md`+ for primary touch actions.
 */
export function IconButton({
  className,
  variant,
  size,
  asChild = false,
  type,
  ...props
}: IconButtonProps) {
  const Comp = asChild ? Slot : "button";
  return (
    <Comp
      data-slot="icon-button"
      className={cn(iconButtonVariants({ variant, size }), className)}
      type={asChild ? undefined : (type ?? "button")}
      {...props}
    />
  );
}
JSON del registryr/icon-button.json
r/icon-button.json
{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "icon-button",
  "type": "registry:ui",
  "title": "IconButton",
  "description": "A square, icon-only action that requires an accessible name.",
  "dependencies": [
    "@radix-ui/react-slot",
    "class-variance-authority"
  ],
  "registryDependencies": [
    "@vexa/utils"
  ],
  "files": [
    {
      "path": "registry/default/ui/icon-button.tsx",
      "type": "registry:ui",
      "target": "components/vexa/icon-button.tsx",
      "content": "import type * as React from \"react\";\nimport { Slot } from \"@radix-ui/react-slot\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\n\nimport { cn } from \"@/lib/utils\";\n\nexport const iconButtonVariants = cva(\n  [\n    \"inline-flex select-none items-center justify-center\",\n    \"rounded-vexa-md text-sm font-medium\",\n    \"transition-[color,background-color,border-color,box-shadow]\",\n    \"duration-[var(--vexa-duration-fast)] ease-[var(--vexa-ease-standard)]\",\n    \"outline-none focus-visible:ring-2 focus-visible:ring-vexa-ring\",\n    \"focus-visible:ring-offset-2 focus-visible:ring-offset-vexa-background\",\n    \"disabled:pointer-events-none disabled:opacity-50\",\n    \"aria-[busy=true]:pointer-events-none aria-[busy=true]:cursor-progress\",\n    \"[&_svg]:pointer-events-none [&_svg]:shrink-0\",\n  ],\n  {\n    variants: {\n      variant: {\n        primary:\n          \"bg-vexa-primary text-vexa-primary-foreground hover:bg-vexa-primary/90 active:bg-vexa-primary/95\",\n        secondary:\n          \"border border-vexa-border bg-vexa-secondary text-vexa-secondary-foreground hover:bg-vexa-accent active:bg-vexa-accent\",\n        outline:\n          \"border border-vexa-input bg-transparent text-vexa-foreground hover:bg-vexa-accent hover:text-vexa-accent-foreground active:bg-vexa-accent\",\n        ghost:\n          \"bg-transparent text-vexa-foreground hover:bg-vexa-accent hover:text-vexa-accent-foreground active:bg-vexa-accent\",\n        danger:\n          \"bg-vexa-danger text-vexa-danger-foreground hover:bg-vexa-danger/90 active:bg-vexa-danger/95\",\n      },\n      size: {\n        sm: \"size-8 [&_svg]:size-4\",\n        md: \"size-10 [&_svg]:size-5\",\n        lg: \"size-12 [&_svg]:size-6\",\n      },\n    },\n    defaultVariants: {\n      variant: \"ghost\",\n      size: \"md\",\n    },\n  },\n);\n\n/**\n * An IconButton MUST have an accessible name. This type enforces at least one\n * of `aria-label` or `aria-labelledby` at compile time, since the button has no\n * visible text label.\n */\ntype AccessibleName =\n  | { \"aria-label\": string; \"aria-labelledby\"?: never }\n  | { \"aria-labelledby\": string; \"aria-label\"?: never };\n\nexport type IconButtonProps = Omit<\n  React.ComponentProps<\"button\">,\n  \"aria-label\" | \"aria-labelledby\"\n> &\n  VariantProps<typeof iconButtonVariants> & {\n    asChild?: boolean;\n  } & AccessibleName;\n\n/**\n * IconButton — a square, icon-only action. Requires an accessible name.\n *\n * RSC-safe. Square touch targets: `md` and `lg` meet the 44×44 CSS-pixel\n * guidance; use `md`+ for primary touch actions.\n */\nexport function IconButton({\n  className,\n  variant,\n  size,\n  asChild = false,\n  type,\n  ...props\n}: IconButtonProps) {\n  const Comp = asChild ? Slot : \"button\";\n  return (\n    <Comp\n      data-slot=\"icon-button\"\n      className={cn(iconButtonVariants({ variant, size }), className)}\n      type={asChild ? undefined : (type ?? \"button\")}\n      {...props}\n    />\n  );\n}\n"
    }
  ],
  "meta": {
    "frameworks": [
      "react",
      "next"
    ],
    "rsc": "server-safe",
    "variants": {
      "variant": [
        "primary",
        "secondary",
        "outline",
        "ghost",
        "danger"
      ],
      "size": [
        "sm",
        "md",
        "lg"
      ]
    },
    "cssVars": [
      "--vexa-primary",
      "--vexa-primary-foreground",
      "--vexa-secondary",
      "--vexa-accent",
      "--vexa-accent-foreground",
      "--vexa-danger",
      "--vexa-danger-foreground",
      "--vexa-border",
      "--vexa-input",
      "--vexa-ring",
      "--vexa-background"
    ],
    "a11y": "Type-level requirement: `aria-label` or `aria-labelledby` is mandatory. Sizes md/lg meet the 44x44 target guidance.",
    "example": "<IconButton aria-label=\"Open menu\"><MenuIcon /></IconButton>",
    "tests": "planned — see docs/README.md#roadmap"
  }
}

Detalles

Variantes

variant
primarysecondaryoutlineghostdanger
size
smmdlg

Accesibilidad

Type-level requirement: `aria-label` or `aria-labelledby` is mandatory. Sizes md/lg meet the 44x44 target guidance.

Uso

A square button whose only content is an icon — toolbars, close affordances, and compact actions where a text label would not fit.

Anatomía

IconButton
Square button expecting a single icon child.
iconButtonVariants
cva helper for the icon-button variant/size classes.

Accesibilidad

Native `<button>`. An accessible name is mandatory and enforced at the type level: pass `aria-label` or `aria-labelledby`.

Teclado

TeclaAcción
Enter / SpaceActivate the button

Foco: Visible `focus-visible` ring; native `disabled` handling.

  • Target Size (WCAG 2.2 §2.5.8): sizes `md`/`lg` meet the 44×44 guidance; avoid `sm` for primary touch targets.

Buenas prácticas

Recomendado

  • Always pass `aria-label`.
  • Pair with a Tooltip for discoverability.
  • Use `md`/`lg` for touch-first UIs.

Evitar

  • Don't ship without an accessible name.
  • Don't place text or multiple glyphs inside.
  • Don't use `sm` as a primary touch target.
Editar esta página

¿Te ha resultado útil esta página?

Guardado localmente en este navegador. Sin cuenta ni servidor.