Skip to content

Btn (icon button)

Source: cairn/ts/src/components/PhotoViewer.jsx · artifex/frontend/src/components/PhotoViewer.jsx Category: UI primitive

Btn — a minimal icon-first button with muted default, brighter hover, and a tinted active state.

Renders a <button> with a square hit area, a 1.5× icon slot, and three visual states: muted default, bright hover, tinted “active” (reads as currently-toggled).

The problem: toolbars re-implemented the same button shape across components — square hit area, 1.5× icon, muted-default / bright-hover / tinted-active states. 30+ lines of duplicated markup across PhotoViewer, CompareView, and header rows.

The fix: one component, deliberately tiny interface — onClick, active, children, passthrough props. Hosts any icon from @phosphor-icons/react or lucide-react without caring which.

src/examples/btn.tsx
interface BtnProps extends Omit<ComponentPropsWithoutRef<'button'>, 'onClick' | 'children'> {
onClick?: MouseEventHandler<HTMLButtonElement>;
active?: boolean;
children: ReactNode;
}
export default function Btn({ onClick, active, children, ...props }: BtnProps) {
return (
<button onClick={onClick} style={{ /* muted / hover / active */ }} {...props}>
{children}
</button>
);
}

See the full example file at src/examples/btn.tsx in this repo.

  • Cairn / PhotoViewer — zoom, favorite, share, and delete toolbar buttons
  • Cairn / CompareView — swap, zoom in/out, fit controls
  • Artifex / PhotoViewer — same shape, slightly different active background
  • Must live at module scope. Defining Btn inside another component’s render body creates a fresh component identity on every parent render — React unmounts and remounts every instance. eslint-plugin-react-hooks catches this with react-hooks/static-components. See artifex/bug-component-in-render-001.
  • No forwardRef. Focus management or DOM-node access from a parent (tooltip anchors, etc.) requires wrapping or switching to React.forwardRef.
  • Hover state uses inline style. Works everywhere, but cannot be themed via CSS variables. Fine for teaching; production code prefers a class-based approach with theme tokens.