Klarhimmel UI

Icon button

Stable

A button with only an icon. The label is its accessible name and its tooltip, so it can never be unnamed.

npx shadcn add @klarhimmel/icon-button

When to use

  • Use for

    Toolbars, table rows and tight corners: close, share, bold.

  • Not for

    Anything a word would explain better. Use Button.

  • Pairs with

    Tooltip (built in), Menu triggers, toolbars.

Preview

Ghost, soft, secondary, primary and disabled. Hover or tab to a button for its name.

icon-button.tsx
import { Bell } from 'lucide-react';
import { Theme } from '@/components/klarhimmel/theme/style';
import { IconButton } from '@/components/klarhimmel/ui/icon-button';

export default function Example() {
  return (
    <Theme theme="brand">
      <IconButton label="Notifications" shortcut="N">
        <Bell aria-hidden className="size-5" />
      </IconButton>
    </Theme>
  );
}

Install

Import

import { IconButton } from '@/components/klarhimmel/ui/icon-button';
Lands in
components/klarhimmel/ui/icon-button.tsx, plus cn() and the theme item if they are missing. Import theme.css after Tailwind.
Render in
Render it inside <Theme>, once near the root of your app. Tokens and fonts are scoped to it, so outside it the component is unstyled.
Limit
Overlays render into the Theme root with position: fixed. A transform on an ancestor re-parents fixed (a dialog centres in the wrapper) and overflow: hidden clips menus. Render <Theme> at the app root, outside any transformed wrapper.

Examples

  • Toolbar

    Bold is on, italic is off.

    import { useState } from 'react';
    import { Bold, Italic, Link2 } from 'lucide-react';
    import { IconButton } from '@/components/klarhimmel/ui/icon-button';
    import { TooltipProvider } from '@/components/klarhimmel/ui/tooltip';
    
    export default function IconButtonToolbar() {
      const [on, setOn] = useState({ bold: true, italic: false });
      return (
        <div className="flex min-h-24 flex-col items-start gap-3">
          <TooltipProvider>
          <div role="toolbar" aria-label="Text style" className="flex gap-1 rounded-kh-lg border border-kh-line bg-kh-surface p-1">
            <IconButton size="sm" label="Bold" shortcut="B" active={on.bold} tooltipSide="bottom" onClick={() => setOn((s) => ({ ...s, bold: !s.bold }))}>
              <Bold aria-hidden className="size-4" />
            </IconButton>
            <IconButton size="sm" label="Italic" shortcut="I" active={on.italic} tooltipSide="bottom" onClick={() => setOn((s) => ({ ...s, italic: !s.italic }))}>
              <Italic aria-hidden className="size-4" />
            </IconButton>
            <IconButton size="sm" label="Add link" tooltipSide="bottom"><Link2 aria-hidden className="size-4" /></IconButton>
          </div>
          </TooltipProvider>
          <p aria-live="polite" className="text-kh-small text-kh-muted">Bold is {on.bold ? 'on' : 'off'}, italic is {on.italic ? 'on' : 'off'}.</p>
        </div>
      );
    }
  • Toggle

    On: bold. A pressed button announces as pressed.

    import { useState } from 'react';
    import { Bold, Italic, Underline } from 'lucide-react';
    import { IconButton } from '@/components/klarhimmel/ui/icon-button';
    import { TooltipProvider } from '@/components/klarhimmel/ui/tooltip';
    
    const marks = [
      { id: 'bold', label: 'Bold', shortcut: 'B', Icon: Bold },
      { id: 'italic', label: 'Italic', shortcut: 'I', Icon: Italic },
      { id: 'underline', label: 'Underline', shortcut: 'U', Icon: Underline },
    ];
    
    export default function IconButtonToggle() {
      const [on, setOn] = useState<string[]>(['bold']);
      return (
        <TooltipProvider>
          <div className="flex flex-col gap-3">
            <div role="group" aria-label="Text style" className="flex items-center gap-1">
              {marks.map(({ id, label, shortcut, Icon }) => (
                <IconButton
                  key={id}
                  label={label}
                  shortcut={shortcut}
                  variant="soft"
                  active={on.includes(id)}
                  onClick={() => setOn((s) => (s.includes(id) ? s.filter((x) => x !== id) : [...s, id]))}
                >
                  <Icon aria-hidden className="size-5" />
                </IconButton>
              ))}
            </div>
            <p aria-live="polite" className="text-kh-small text-kh-muted">On: {on.join(', ') || 'nothing'}. A pressed button announces as pressed.</p>
          </div>
        </TooltipProvider>
      );
    }

API reference

PropTypeDefaultDescription
labelstring-Required. The accessible name, also shown as the tooltip.
variant'ghost' | 'soft' | 'secondary' | 'primary''ghost'Visual style.
size'sm' | 'md' | 'lg''md'The small size is 36 px with a 44 px hit area.
activeboolean-Makes it a toggle: sets aria-pressed and the pressed look.
shortcutstring-A shortcut shown in the tooltip.
noTooltipbooleanfalseSkip the tooltip when the label is already visible nearby.
tooltipSide / tooltipDelay'top' | 'right' | 'bottom' | 'left' / number'top'Where the tooltip opens and the hover delay in ms.
...buttonButtonHTMLAttributes-Native attributes are passed through; type defaults to "button".
classNamestring-Extra classes, merged after the component's own.

Accessibility

KeyDoes
Enter / SpaceActivates the button
TabMoves focus; the tooltip shows with it
EscapeHides the tooltip

The label is required: it is the only name a screen reader hears.

Accessibility