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.
What it is
Section titled “What it is”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).
Why it exists
Section titled “Why it exists”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.
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.
How it’s used
Section titled “How it’s used”- 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
Gotchas
Section titled “Gotchas”- Must live at module scope. Defining
Btninside another component’s render body creates a fresh component identity on every parent render — React unmounts and remounts every instance.eslint-plugin-react-hookscatches this withreact-hooks/static-components. Seeartifex/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.