A button with one icon and no text, in ghost or outline, or as a link.

Variants

Ghost has no fill and no edge. Outline draws a 1px ink edge, for a button that has to show where it is.

Example

Follows the page theme and corners
'use client';

import { IconButton } from '@snapshot-ui/react';
import { Bell, Search, Settings } from 'lucide-react';
import type { ReactElement } from 'react';

export default function IconButtonVariants(): ReactElement {
  return (
    <div style={{ display: 'grid', gap: 16 }}>
      <div className="row" style={{ justifyContent: 'center', gap: 12 }}>
        <IconButton aria-label="Search">
          <Search />
        </IconButton>
        <IconButton aria-label="Notifications">
          <Bell />
        </IconButton>
        <IconButton aria-label="Settings">
          <Settings />
        </IconButton>
      </div>
      <div className="row" style={{ justifyContent: 'center', gap: 12 }}>
        <IconButton variant="outline" aria-label="Search">
          <Search />
        </IconButton>
        <IconButton variant="outline" aria-label="Notifications">
          <Bell />
        </IconButton>
        <IconButton variant="outline" aria-label="Settings">
          <Settings />
        </IconButton>
      </div>
    </div>
  );
}

States

Hover and press wash the circle with a little ink, in dark as well. Disabled turns the icon taupe and ignores presses.

Example

Follows the page theme and corners
'use client';

import { IconButton } from '@snapshot-ui/react';
import { Check, Copy } from 'lucide-react';
import { useEffect, useRef, useState, type ReactElement } from 'react';

export default function IconButtonStates(): ReactElement {
  const [copied, setCopied] = useState(false);
  const timer = useRef(0);
  useEffect(
    () => () => {
      window.clearTimeout(timer.current);
    },
    [],
  );
  return (
    <div className="row" style={{ justifyContent: 'center', gap: 12 }}>
      <IconButton
        aria-label="Copy"
        onPress={() => {
          setCopied(true);
          window.clearTimeout(timer.current);
          timer.current = window.setTimeout(() => {
            setCopied(false);
          }, 2000);
        }}
      >
        {copied ? <Check /> : <Copy />}
      </IconButton>
      <span className="visually-hidden" role="status">
        {copied ? 'Copied' : ''}
      </span>
      <IconButton aria-label="Copy" isDisabled>
        <Copy />
      </IconButton>
      <IconButton variant="outline" aria-label="Copy" isDisabled>
        <Copy />
      </IconButton>
    </div>
  );
}

Give it an href to render an anchor with the same look, which still needs a name. Use the link form for an icon that goes somewhere, and the button form for one that does something. The link form follows the app's router, as set up in Routing.

Example

Follows the page theme and corners
'use client';

import { IconButton } from '@snapshot-ui/react';
import { ExternalLink } from 'lucide-react';
import type { ReactElement } from 'react';

export default function IconButtonLink(): ReactElement {
  return (
    <div className="row" style={{ justifyContent: 'center', gap: 12 }}>
      <IconButton href="https://github.com/amirehsan/snapshot-ui" aria-label="Snapshot UI on GitHub">
        <ExternalLink />
      </IconButton>
      <IconButton
        variant="outline"
        href="https://github.com/amirehsan/snapshot-ui"
        aria-label="Snapshot UI on GitHub"
      >
        <ExternalLink />
      </IconButton>
    </div>
  );
}

Usage

The child is the icon. It is hidden from assistive technology, so the name comes from aria-label or aria-labelledby, and the types require one of them.

import { IconButton } from '@snapshot-ui/react';
import { Settings } from 'lucide-react';

export function SettingsButton({ onOpen }: { onOpen: () => void }) {
  return (
    <IconButton aria-label="Open settings" onPress={onOpen}>
      <Settings />
    </IconButton>
  );
}

Props

Every React Aria Button prop is accepted, or every Link prop when there is an href. These are the ones Snapshot UI adds or changes.

aria-label
string

A name for assistive technology. Give it, or aria-labelledby, where no label or text is shown.

aria-labelledby
string

The id of an element that holds the name, such as a label shown elsewhere.

href
string

A URL to link to. Given an href, the icon button renders a link instead of a button.

onPress
((e: PressEvent) => void)

Handler that is called when the press is released over the target.

variant
IconButtonVariant

ghost has no fill and no edge. outline is the design system's circle on cream: a transparent fill with a 1px ink edge.

Default: 'ghost'

Accessibility

A native button element, or an anchor when there is an href, with React Aria handling press across mouse, touch and keyboard. It is 40px square, which meets the touch target on its own.

SpaceEnterActivates the button.
EnterFollows the link, when it has an href.
TabMoves focus to it. The 2px ink ring sits outside the circle.

Design rules

What keeps icon buttons in the system.

Name every one for its action, such as Open the menu or Copy code.

Use ghost in bars and headers, and outline where the button needs an edge to read as a button.

Never use one for the main action of a view. A Button with a label says what it does.

Never put text in it. If it needs words, use a Button.

esc

↑ ↓ to move↵ to open