diff --git a/components/ui/__tests__/icon-button.test.tsx b/components/ui/__tests__/icon-button.test.tsx
new file mode 100644
index 000000000..39efa0367
--- /dev/null
+++ b/components/ui/__tests__/icon-button.test.tsx
@@ -0,0 +1,31 @@
+import { createRef } from 'react';
+import { fireEvent, render, screen } from '@testing-library/react';
+import { describe, expect, it, vi } from 'vitest';
+import { IconButton } from '../icon-button';
+import { X } from '@/components/icons';
+
+describe('IconButton', () => {
+ it('is named and titled by its label', () => {
+ render();
+ const button = screen.getByRole('button', { name: 'Close' });
+ expect(button).toHaveAttribute('title', 'Close');
+ expect(button).toHaveAttribute('type', 'button');
+ });
+
+ it('defaults to ghost icon-sm and takes another size', () => {
+ const { rerender } = render();
+ expect(screen.getByRole('button')).toHaveClass('h-8', 'w-8', 'hover:bg-accent');
+ rerender();
+ expect(screen.getByRole('button')).toHaveClass('h-11', 'w-11');
+ });
+
+ it('forwards the ref and other props', () => {
+ const ref = createRef();
+ const onClick = vi.fn();
+ render();
+ expect(ref.current).toBe(screen.getByTestId('del'));
+ expect(ref.current).not.toHaveAttribute('title', 'Delete');
+ fireEvent.click(ref.current!);
+ expect(onClick).toHaveBeenCalledOnce();
+ });
+});
diff --git a/components/ui/icon-button.tsx b/components/ui/icon-button.tsx
new file mode 100644
index 000000000..92939713c
--- /dev/null
+++ b/components/ui/icon-button.tsx
@@ -0,0 +1,33 @@
+import * as React from "react";
+import { Button, type ButtonProps } from "@/components/ui/button";
+
+export interface IconButtonProps extends Omit {
+ /** What the button does. Becomes the accessible name and the tooltip. */
+ label: string;
+ /** The icon. It gets w-4 in icon-sm and w-5 in icon-lg unless it sets its own size. */
+ children: React.ReactNode;
+}
+
+/**
+ * An icon-only button (R1): ghost, 32px (`icon-sm`) unless told otherwise,
+ * always named. `title` defaults to the label; pass `title=""` to drop the
+ * tooltip where another one already shows.
+ */
+const IconButton = React.forwardRef(
+ ({ label, variant = "ghost", size = "icon-sm", type = "button", title, children, ...props }, ref) => (
+
+ )
+);
+IconButton.displayName = "IconButton";
+
+export { IconButton };