Skip to content
v2.5.0
English
Русский
Documentation

IconButton

IconButton is the icon-only action variant built on the same button foundation as Button.
icon-button.tsx · 7 lines
import { Icon, IconButton } from '@vega-ui/react';
import { Heart, Star } from '@vega-ui/icons';

<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
  <IconButton aria-label='Add to favorites'><Icon><Heart /></Icon></IconButton>
  <IconButton variant='secondary' appearance='ghost' aria-label='Star project'><Icon><Star /></Icon></IconButton>
</div>
Browser support
Newly available 2024
  • Chrome 125
  • Edge 125
  • Firefox 128
  • Safari 18
  • Safari on iOS 18
Bundle size
Tested in
15 tests

Summary

IconButton is the icon-only action variant built on the same button foundation as Button. It is the compact action layer when visible text would add noise rather than clarity.

Imports

import {
  IconButton,
  type IconButtonProps,
  type IconButtonSize,
} from '@vega-ui/react';

Exported Types

  • IconButtonProps
  • IconButtonSize

Basic Usage

basic-usage.tsx · 27 lines
import { Icon, IconButton, Text } from '@vega-ui/react';
import { EllipsisIcon, PencilIcon, Share2Icon } from '@vega-ui/icons';
import { useState } from 'react';

export default function BasicUsage() {
  const [lastAction, setLastAction] = useState('');

  return (
    <div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--spacing-2)', width: '100%' }}>
      <header style={{ display: 'flex', alignItems: 'center', gap: 'var(--spacing-2)' }}>
        <Text size={4} fontWeight={600} style={{ flex: 1, minWidth: 0 }}>Brand guidelines</Text>
        <IconButton type='button' size='sm' variant='secondary' appearance='transparent' aria-label='Rename document' onClick={() => setLastAction('Rename')}>

Variants

  • Sizes: xs, sm, md, lg, xl
  • Variants and appearances inherited from the button foundation
  • Composition: default button or asChild

Edge Cases

  • Always provide an aria-label because visible text is usually absent.
  • Keep icon glyph size visually balanced with the button size token.
  • Use regular Button when visible text would improve clarity.
  • If the icon meaning depends on surrounding context too heavily, a text button is usually the safer choice.

Common Mistakes

  • Omitting aria-label.
  • Putting long text inside IconButton.
  • Using icon-only actions where the meaning is not already recognizable.

Examples

Basic

Basic: header actions with explicit names

Every icon-only action needs its own aria-label: the component never adds one. Inside IconButton an Icon without size scales with the button through --icon-size.

basic-usage.tsx · 27 lines
import { Icon, IconButton, Text } from '@vega-ui/react';
import { EllipsisIcon, PencilIcon, Share2Icon } from '@vega-ui/icons';
import { useState } from 'react';

export default function BasicUsage() {
  const [lastAction, setLastAction] = useState('');

  return (
    <div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--spacing-2)', width: '100%' }}>
      <header style={{ display: 'flex', alignItems: 'center', gap: 'var(--spacing-2)' }}>
        <Text size={4} fontWeight={600} style={{ flex: 1, minWidth: 0 }}>Brand guidelines</Text>
        <IconButton type='button' size='sm' variant='secondary' appearance='transparent' aria-label='Rename document' onClick={() => setLastAction('Rename')}>

Basic: sizes

size sets the square side from xs to xl; the nested icon follows at one third of the button size.

sizes.tsx · 16 lines
import { Icon, IconButton, type IconButtonSize } from '@vega-ui/react';
import { SettingsIcon } from '@vega-ui/icons';

const sizes: IconButtonSize[] = ['xs', 'sm', 'md', 'lg', 'xl'];

export default function Sizes() {
  return (
    <div style={{ display: 'flex', alignItems: 'center', gap: 'var(--spacing-4)' }}>
      {sizes.map((size) => (
        <IconButton key={size} type='button' size={size} aria-label={`Settings, size ${size}`}>
          <Icon><SettingsIcon /></Icon>
        </IconButton>

Controlled/Stateful

Controlled/Stateful: favorite toggle with aria-pressed

The label stays "Favorite" and the state lives in aria-pressed, so a screen reader announces "Favorite, toggle button, pressed". Changing the label on every click would hide the state instead.

parent-owned-favorite-toggle.tsx · 24 lines
import { Icon, IconButton } from '@vega-ui/react';
import { HeartIcon } from '@vega-ui/icons';
import { useState } from 'react';

export default function ParentOwnedFavoriteToggle() {
  const [favorite, setFavorite] = useState(false);
  const count = favorite ? 129 : 128;

  return (
    <div style={{ display: 'flex', alignItems: 'center', gap: 'var(--spacing-4)', width: '100%' }}>
      <IconButton
        type='button'

Controlled/Stateful: copy with temporary feedback

The icon swaps to a check for two seconds after copying, and a status region announces "Copied", because an icon change alone is silent for screen readers.

copy-to-clipboard.tsx · 48 lines
import { Icon, IconButton, VisuallyHidden } from '@vega-ui/react';
import { CheckIcon, CopyIcon } from '@vega-ui/icons';
import { useEffect, useState } from 'react';

const inviteLink = 'https://app.example.com/invite/7Fq2-K9xD';

export default function CopyToClipboard() {
  const [copied, setCopied] = useState(false);

  useEffect(() => {
    if (!copied) return;

Form/Integration

Form/Integration: clear and submit actions next to an input

The clear button uses type='button' so it does not submit the form, and returns focus to the input. The search button is a real type='submit'; the query is read with FormData.

inline-clear-action.tsx · 55 lines
import type { CSSProperties } from 'react';
import { Icon, IconButton, Label, TextField, TextFieldInput } from '@vega-ui/react';
import { SearchIcon, XIcon } from '@vega-ui/icons';
import { FormEvent, useRef, useState } from 'react';

export default function InlineClearAction() {
  const input = useRef<HTMLInputElement>(null);
  const [query, setQuery] = useState('quarterly report');
  const [submitted, setSubmitted] = useState('');

  const clear = () => {
    setQuery('');

Layout/Overlay

Layout/Overlay: close button in a dialog

The standard overlay close: a 24×24 round ghost button inside DialogClose asChild, positioned with logical insets so it flips in right-to-left layouts.

compact-close-action.tsx · 41 lines
import type { CSSProperties } from 'react';
import {
  Button,
  Dialog,
  DialogClose,
  DialogContent,
  DialogTrigger,
  Heading,
  Icon,
  IconButton,
  Text,
} from '@vega-ui/react';

Layout/Overlay: tooltip for sighted users

aria-label names the button for assistive technology; a Tooltip shows the same name and the shortcut to mouse and keyboard users. The tooltip is linked as a description, so keep extra details there.

tooltip-label.tsx · 50 lines
import { Icon, IconButton, Tooltip, TooltipArrow, TooltipContent, TooltipTrigger } from '@vega-ui/react';
import { AlignCenterIcon, AlignLeftIcon, AlignRightIcon } from '@vega-ui/icons';
import { type ReactNode, useState } from 'react';

const alignments: { value: string, label: string, shortcut: string, icon: ReactNode }[] = [
  { value: 'left', label: 'Align left', shortcut: 'Ctrl+Shift+L', icon: <AlignLeftIcon /> },
  { value: 'center', label: 'Align center', shortcut: 'Ctrl+Shift+E', icon: <AlignCenterIcon /> },
  { value: 'right', label: 'Align right', shortcut: 'Ctrl+Shift+R', icon: <AlignRightIcon /> },
];

export default function TooltipLabel() {
  const [alignment, setAlignment] = useState('left');

Error

Error: remove action that fails

Removing a locked file fails: the error is rendered under the row and linked to its button with aria-describedby. The accessible name includes the file name, so ten identical trash icons are still distinguishable.

failed-remove-action.tsx · 80 lines
import { HelperText, Icon, IconButton } from '@vega-ui/react';
import { FileTextIcon, Trash2Icon } from '@vega-ui/icons';
import { useState } from 'react';

const initialFiles = [
  { name: 'contract-signed.pdf', locked: true },
  { name: 'brief-v3.docx', locked: false },
  { name: 'moodboard.png', locked: false },
];

export default function FailedRemoveAction() {
  const [files, setFiles] = useState(initialFiles);

Disabled

Disabled: toolbar disabled by a fieldset

<Fieldset disabled> disables every IconButton inside it natively, which suits read-only documents where the whole toolbar is unavailable at once.

blocked-icon-action.tsx · 32 lines
import { Button, Fieldset, FieldsetLegend, Icon, IconButton } from '@vega-ui/react';
import { BoldIcon, ItalicIcon, LinkIcon, ListIcon } from '@vega-ui/icons';
import { useState } from 'react';

export default function BlockedIconAction() {
  const [locked, setLocked] = useState(true);

  return (
    <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-start', gap: 'var(--spacing-8)', width: '100%' }}>
      <Fieldset disabled={locked} appearance='outlined' style={{ width: '100%', boxSizing: 'border-box' }}>
        <FieldsetLegend>{locked ? 'Formatting (read-only document)' : 'Formatting'}</FieldsetLegend>
        <div style={{ display: 'flex', gap: 'var(--spacing-2)' }}>

Edge

Edge: zoom controls at their limits

The buttons are disabled at the first and last zoom step, computed from the list of steps. When a button disables itself while focused, the browser drops focus, so place the value between the buttons where the user can still see it.

zoom-controls.tsx · 41 lines
import { Icon, IconButton } from '@vega-ui/react';
import { MinusIcon, PlusIcon } from '@vega-ui/icons';
import { useState } from 'react';

const steps = [25, 50, 75, 100, 150, 200, 400];

export default function ZoomControls() {
  const [index, setIndex] = useState(3);
  const atMin = index === 0;
  const atMax = index === steps.length - 1;

  return (

Anatomy

Overview

IconButton is a single icon-only action surface built on ButtonBase. Its anatomy is one interactive root plus icon content with no visible text label.

Required Parts

IconButton

Required. Owns size and button-base visual props such as variant and appearance.

Optional Parts

IconButton does not expose public child parts, but consumers usually compose it with Icon.

Composition Order

  1. IconButton
  2. icon content

Valid Composition Patterns

basic-usage.tsx · 27 lines
import { Icon, IconButton, Text } from '@vega-ui/react';
import { EllipsisIcon, PencilIcon, Share2Icon } from '@vega-ui/icons';
import { useState } from 'react';

export default function BasicUsage() {
  const [lastAction, setLastAction] = useState('');

  return (
    <div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--spacing-2)', width: '100%' }}>
      <header style={{ display: 'flex', alignItems: 'center', gap: 'var(--spacing-2)' }}>
        <Text size={4} fontWeight={600} style={{ flex: 1, minWidth: 0 }}>Brand guidelines</Text>
        <IconButton type='button' size='sm' variant='secondary' appearance='transparent' aria-label='Rename document' onClick={() => setLastAction('Rename')}>

Invalid Composition Patterns

Icon-only button without accessible label

The action becomes unclear for assistive technology and often for sighted users too.

Long text content inside IconButton

If the action needs visible words, use Button instead.