# Components

The kit components the grid renders its own chrome from — Button and IconButton — how to style them, and how to replace either with your own.


`@pretable/react` renders its own controls from a small component kit, and
ships those components for you to use and to replace. This page covers the
first two: `PretableButton` and `PretableIconButton`. Here is a grid whose
every labelled button is the app's own:

### Example: Replacing a component

The grid's own tool panel and filter dialog, with every Button replaced by the app's — one slot, applied everywhere, branching on site for the one place it treats differently.

Source: https://pretable.ai/examples/components-button.md

```tsx ComponentsGrid.tsx
"use client";

import { PretableSurface } from "@pretable/react";

import "./brand-button.css";

import { BrandButton } from "./BrandButton";
import { columns } from "./columns";
import { trades, type Trade } from "./data";

const VIEWPORT_HEIGHT = 260;

export function ComponentsGrid() {
  return (
    <div>
      <p style={{ margin: "0 0 8px", fontSize: 13 }}>
        Every labelled button below is <code>BrandButton</code>; the icon
        buttons — the funnels, the ⋮ menus — are still pretable&apos;s, because
        this demo replaces only <code>Button</code>. Reset columns is the
        app&apos;s danger button (it branches on <code>site</code>); the Filters
        section&apos;s <code>+ filter</code> / <code>+ group</code> are its
        ghost buttons; a column funnel&apos;s dialog has its link-styled{" "}
        <code>Clear</code>.
      </p>
      <PretableSurface<Trade>
        ariaLabel="Trades"
        columns={columns}
        components={{ Button: BrandButton }}
        getRowId={(row) => row.id}
        rows={trades}
        toolPanel={{ defaultActiveSection: "columns" }}
        viewportHeight={VIEWPORT_HEIGHT}
      />
    </div>
  );
}
```

```tsx BrandButton.tsx
"use client";

import { forwardRef } from "react";
import type { PretableButtonProps } from "@pretable/react";

/**
 * The app's own button, standing in for every Button the grid renders. It
 * receives exactly what pretable's does — `site` included — and forwards its
 * ref, which is the one thing the grid asks of a replacement: menus anchor on
 * the node, and focus returns to it.
 */
export const BrandButton = forwardRef<HTMLButtonElement, PretableButtonProps>(
  function BrandButton({ site, variant, className, ...props }, ref) {
    // One place treated differently: the reset is destructive, so it gets the
    // app's danger styling. Everything else is the brand default.
    const tone = site === "tool-reset" ? "danger" : (variant ?? "ghost");
    return (
      <button
        {...props}
        ref={ref}
        type="button"
        className={["brand-button", `brand-button--${tone}`, className]
          .filter(Boolean)
          .join(" ")}
      />
    );
  },
);
```

```css brand-button.css
.brand-button {
  border: 0;
  border-radius: 999px;
  padding: 0 12px;
  height: 26px;
  font: inherit;
  font-weight: 600;
  cursor: pointer;
}

.brand-button--ghost {
  background: #eef2ff;
  color: #3730a3;
}

.brand-button--link {
  background: transparent;
  color: #3730a3;
  padding: 0 4px;
}

.brand-button--danger {
  background: #fee2e2;
  color: #991b1b;
}

.brand-button:disabled {
  opacity: 0.5;
  cursor: default;
}
```

```ts columns.ts
import type { PretableColumn } from "@pretable/react";

import type { Trade } from "./data";

export const columns: PretableColumn<Trade>[] = [
  { id: "symbol", header: "Symbol", widthPx: 110, type: "text" },
  { id: "side", header: "Side", widthPx: 90, type: "enum" },
  { id: "qty", header: "Qty", widthPx: 90, type: "number" },
  { id: "price", header: "Price", widthPx: 110, type: "number" },
];
```

```ts data.ts
export interface Trade {
  id: string;
  symbol: string;
  side: "Buy" | "Sell";
  qty: number;
  price: number;
}

export const trades: Trade[] = [
  { id: "t1", symbol: "AAPL", side: "Buy", qty: 100, price: 226.11 },
  { id: "t2", symbol: "MSFT", side: "Sell", qty: 40, price: 418.38 },
  { id: "t3", symbol: "NVDA", side: "Buy", qty: 25, price: 869.63 },
  { id: "t4", symbol: "AMZN", side: "Buy", qty: 60, price: 183.91 },
  { id: "t5", symbol: "META", side: "Sell", qty: 15, price: 509.62 },
];
```


## The components

Both render `<button type="button">`, always — every grid button is one, and a
stray submit inside your `<form>` is a real bug class, so `type` is not a prop.
`className` and `style` pass straight through — the components set neither,
so yours is the whole value. Both forward their `ref` to the button node.

### Button

A labelled push-button in one of the grid's two looks.

| Prop      | Type                 | Notes                                                                                       |
| --------- | -------------------- | ------------------------------------------------------------------------------------------- |
| `variant` | `"ghost" \| "link"`  | `ghost` is the 24px action with a hover tint; `link` is plain accent text. Default `ghost`. |
| `site`    | `PretableButtonSite` | Where in the grid the button is; lands as `data-pretable-site`. Open to your own names.     |

### IconButton

An icon-only push-button. `aria-label` is **required** — it is the button's
only accessible name, and omitting it is a compile error rather than an
accessibility failure discovered later. An empty one warns in development.

| Prop         | Type                 | Notes                                                           |
| ------------ | -------------------- | --------------------------------------------------------------- |
| `aria-label` | `string`             | Required. The accessible name.                                  |
| `site`       | `PretableButtonSite` | Where in the grid the button is; lands as `data-pretable-site`. |

## Styling

The styling channel is the one the whole grid uses: attributes and tokens.
Every button carries `data-pretable-button` or `data-pretable-icon-button`; a
labelled button also carries `data-pretable-variant`, and either carries
`data-pretable-site` when the grid gave it one. The grid's own sites also
keep their original attribute (`data-pretable-filter-clear`,
`data-pretable-tool-reset`, …), so a selector you already wrote keeps matching.

```css
[data-pretable-button][data-pretable-variant="ghost"] {
  --pretable-accent: rebeccapurple;
}
[data-pretable-site="tool-reset"] {
  color: var(--pretable-text-error);
}
```

## Replacing a component

Pass `components` to `<PretableSurface>` (or `<Pretable>`) with one entry per
component type you want to replace. The replacement is used everywhere that
type appears — the tool panel and the portalled filter dialog for `Button`;
the header, the tool panel and the group panel for `IconButton` — and
receives exactly the props the built-in does, `site` included, so it can
branch on where it is.

| Prop         | Type                          | Notes                                                                          |
| ------------ | ----------------------------- | ------------------------------------------------------------------------------ |
| `Button`     | `PretableButtonComponent`     | Receives `PretableButtonProps`; must forward its `ref` to the button node.     |
| `IconButton` | `PretableIconButtonComponent` | Receives `PretableIconButtonProps`; must forward its `ref` to the button node. |

Forwarding the `ref` is the one obligation on a replacement: the grid anchors
menus on the node and returns keyboard focus to it. Under React 18 that means
`forwardRef`; under React 19 a plain `ref` prop is enough. The set of built-in
`site` names is the `PretableBuiltInButtonSite` type and grows additively — a
new grid button may introduce a new site without a major bump.
