# The slot model

> How slotsmith components are built from slots: element and widget parts, data-* state, wrapped fallbacks, app-wide overrides, compound parts and headless hooks.

The slot model is how slotsmith components are built: each is made of named slots, and its `components` prop replaces any of them. An element part receives one element's props; a widget part is told the state and renders what it likes. Slots you leave alone keep their fallbacks.

## The data table, part by part

Turn on "Show parts" to pull the table apart. The list under it names every slot the data table has, read from its source when the site is built; each links to the slot's row on the [API page](https://slotsmith.dev/ar/components/data-table/api/#slots).

## Two kinds

**Element parts** stand for one element. `<Row>` gets the props of a `<tr>`, `<Cell>` those of a `<td>`, `<Root>` those of a `<div>`. Pass a component that renders that element and spread every prop onto it: they carry the classes, the `data-*` state below, the handlers and the ARIA attributes the table relies on.

```tsx
import { DataTable, type CellSlotProps, type RowSlotProps } from "slotsmith/data-table";
import { columns, people } from "../shared/people";

// An element part gets the element's own props. Spread all of them: they
// carry the data-* state, the handlers and the ARIA attributes.
function Row({ className, ...props }: RowSlotProps) {
  return <tr {...props} className={["app-row", className].filter(Boolean).join(" ")} />;
}

function Cell({ className, ...props }: CellSlotProps) {
  return <td {...props} className={["app-cell", className].filter(Boolean).join(" ")} />;
}

export default function ElementPart() {
  return <DataTable data={people} columns={columns} components={{ Row, Cell }} />;
}
```

A component that already takes those props, such as shadcn/ui's `TableRow` or MUI's `TableRow`, fits as it is: `components={{ Row: TableRow }}`. `<DragHandle>` is an element part too, a `<button>` whose props are the row reorder engine's handlers, so spreading them is what makes the drag work.

**Widget parts** stand for a control. `{Checkbox}` gets `checked`, `indeterminate`, `disabled`, `onCheckedChange` and its `aria-label`; `{Pagination}` gets the page, the page count, whether a previous or next page exists and the functions that go there. The markup is yours, so a design system's own checkbox or pager plugs in through a small adapter.

```tsx
import { DataTable, type PaginationSlotProps } from "slotsmith/data-table";
import { columns, people } from "../shared/people";

// A widget part is told what is true (the page, the page count, what can
// happen next) and decides the markup itself.
function Pager({ pageIndex, pageCount, canPreviousPage, canNextPage, previousPage, nextPage, labels }: PaginationSlotProps) {
  return (
    <nav aria-label="Pages" className="app-pager">
      <button type="button" onClick={previousPage} disabled={!canPreviousPage} aria-label={labels.previousPage}>
        ←
      </button>
      <span>{labels.pageInfo(pageIndex + 1, pageCount)}</span>
      <button type="button" onClick={nextPage} disabled={!canNextPage} aria-label={labels.nextPage}>
        →
      </button>
    </nav>
  );
}

export default function WidgetPart() {
  return <DataTable data={people} columns={columns} components={{ Pagination: Pager }} />;
}
```

The data table has 12 element parts and 10 widget parts. Each slot's props are listed on the [API page](https://slotsmith.dev/ar/components/data-table/api/#slots), and the [adapters](https://slotsmith.dev/ar/components/data-table/adapters/) show the full maps for five design systems.

## Data state

Element parts show their state as `data-*` attributes rather than class names, so you style them with attribute selectors and any design system that already reads them (shadcn/ui's `TableRow` styles `data-[state=selected]`) picks them up. From the data table's source:

- `<Root>`: `data-status` is `loading`, `error`, `empty` or `ready`; `data-size`, `data-striped`, `data-virtual` for the virtual table, and `aria-busy` while loading.
- `<HeaderCell>`: `data-sorted` is `asc` or `desc` on the sorted column, beside `aria-sort`; `data-align` follows the column's `meta.align`.
- `<Row>`: body rows carry `data-state="selected"`, `data-depth`, `data-expanded` (`true` or `false` on rows that can expand), `data-clickable` with `onRowClick`, and `data-row-id`. During a drag they add `data-dragging`, `data-dragging-child`, `data-drop-position` and `data-drop-edge`. The status rows carry `data-state="loading"`, `"empty"` or `"error"`.
- `<Cell>`: `data-align`, and `data-slot="select"` or `"drag"` on the checkbox and drag-handle cells.

```css
/* Selected body rows. */
.app-table tbody tr[data-state="selected"] {
  background: var(--selected-row);
}

/* Status rows: skeletons while loading, the empty and error messages. */
.app-table tr[data-state="loading"] td {
  opacity: 0.6;
}

/* The column the table is sorted by: data-sorted is "asc" or "desc". */
.app-table th[data-sorted] {
  color: var(--accent);
}

/* Tree rows: data-depth is the level, data-expanded "true" or "false". */
.app-table tr[data-expanded="true"] td:first-child {
  font-weight: 600;
}

/* The whole table while it loads, is empty or has failed. */
.app-table[data-status="loading"] {
  cursor: progress;
}
```

Widget parts get the same facts as props instead, such as the `direction` of `{SortIcon}`. The API page lists every attribute under its slot.

## Wrap fallbacks

The built-in parts are exported as `fallbackComponents`. To change one thing about a part, render its fallback inside your own component rather than rewriting it: add a class, put something around it, or pass different props.

```tsx
import { DataTable, fallbackComponents, type EmptySlotProps, type RowSlotProps } from "slotsmith/data-table";
import { columns } from "../shared/people";

// Keep a fallback and change one thing: add a class to the stock row.
function Row(props: RowSlotProps) {
  return <fallbackComponents.Row {...props} className={["app-row", props.className].filter(Boolean).join(" ")} />;
}

// Or put something around it: the stock empty message, with a way out under it.
function Empty(props: EmptySlotProps) {
  return (
    <>
      <fallbackComponents.Empty {...props} />
      <a href="#invite">Invite someone</a>
    </>
  );
}

export default function WrapFallback() {
  return <DataTable data={[]} columns={columns} components={{ Row, Empty }} />;
}
```

Merge the `className` you are given with your own, as the sample does, or the fallback loses its own class and with it the stylesheet's rules.

## Once, for the whole app

To replace a slot everywhere, set it once on `SlotsmithProvider` instead of on every component. Its `components` prop takes one map per component, under the keys `dataTable`, `autocomplete`, `datePicker` and `fileUploader`. Each is the same map that component's own `components` prop takes, so an [adapter](https://slotsmith.dev/ar/components/data-table/adapters/) works in either place.

Below, one provider gives every data table a round checkbox and the date picker a diamond icon. The second table passes its own `Checkbox`, and that one wins:

```tsx
import { useState } from "react";
import { DataTable, type CheckboxSlotProps } from "slotsmith/data-table";
import { DatePicker, type DpIconProps, type ISODate } from "slotsmith/date-picker";
import { SlotsmithProvider, type SlotsmithComponents } from "slotsmith/provider";
import { columns, people } from "../shared/people";

// A widget part of the app's own: a checkbox drawn as a button.
function Check({ radius, checked, indeterminate, disabled, onCheckedChange, "aria-label": label }: CheckboxSlotProps & { radius: string }) {
  return (
    <button
      type="button"
      role="checkbox"
      aria-checked={indeterminate ? "mixed" : checked}
      aria-label={label}
      disabled={disabled}
      onClick={() => onCheckedChange(!checked)}
      style={{ inlineSize: "1.5rem", blockSize: "1.5rem", padding: 0, border: "1.5px solid currentColor", borderRadius: radius, background: "transparent", color: "inherit", lineHeight: 1, cursor: "pointer" }}
    >
      {indeterminate ? "–" : checked ? "✓" : ""}
    </button>
  );
}

const RoundCheckbox = (props: CheckboxSlotProps) => <Check {...props} radius="50%" />;
const SquareCheckbox = (props: CheckboxSlotProps) => <Check {...props} radius="4px" />;

// The date picker's icon: a diamond that fills while the calendar is open.
const DiamondIcon = ({ open }: DpIconProps) => (
  <svg viewBox="0 0 24 24" width="16" height="16" aria-hidden="true" fill={open ? "currentColor" : "none"} stroke="currentColor" strokeWidth="2">
    <path d="M12 3 21 12 12 21 3 12Z" />
  </svg>
);

// One map per component, defined outside any component so it keeps its identity.
const components: SlotsmithComponents = {
  dataTable: { Checkbox: RoundCheckbox },
  datePicker: { Icon: DiamondIcon },
};

// The second table's own prop: it wins over the provider for this slot only.
const squareCheckbox = { Checkbox: SquareCheckbox };

// Three people and three columns keep the two tables short.
const team = people.slice(0, 3);
const teamColumns = columns.slice(0, 3);

export default function ProviderComponents() {
  const [day, setDay] = useState<ISODate | null>(null);

  return (
    <SlotsmithProvider components={components}>
      <div style={{ display: "grid", gap: "0.75rem" }}>
        <div style={{ maxInlineSize: "18rem" }}>
          <DatePicker value={day} onChange={setDay} placeholder="Pick a date" aria-label="Start date" />
        </div>
        <p style={{ margin: 0 }}>Set on the provider: round.</p>
        <DataTable data={team} columns={teamColumns} enableRowSelection hidePagination />
        <p style={{ margin: 0 }}>Set on this table: square.</p>
        <DataTable data={team} columns={teamColumns} enableRowSelection hidePagination components={squareCheckbox} />
      </div>
    </SlotsmithProvider>
  );
}
```

Each slot resolves on its own, in three layers, and the later one wins:

1. The built-in fallback.
2. The `components` of the providers above, for that component.
3. The component's own `components` prop.

- **Nested providers merge.** An inner provider adds to the outer one component by component, then slot by slot: its slots win, and what it does not name is inherited.
- **The variants read the same maps.** `VirtualDataTable`, `VirtualAutocomplete` and `VirtualFileUploader` use their component's key, and so does a layout built from the compound parts.
- **The page-size select follows `autocomplete`.** The data table's built-in page-size select is a slotsmith `Autocomplete`, so it renders with the provider's `autocomplete` slots. A `PageSizeSelect` slot, from `dataTable` or the table's own prop, replaces it altogether.
- **Keep the map's identity.** Define it outside your component, as the sample does, or memoize it. `slotProps` stay on each component.

`SlotsmithProvider` and the types `SlotsmithComponents` and `SlotsmithComponentName` come from `slotsmith/provider`. The same provider sets the [language](https://slotsmith.dev/ar/languages/#passing-a-locale).

## Compound parts

`<DataTable>` is a fixed layout: a root holding the table and the pagination. For another layout, build it from the compound parts inside `DataTable.Provider`. The provider takes the data, the state options and `components`, `labels`, `locale`, `slotProps` and `onRowClick`, and renders no element of its own; `size`, `striped` and HTML attributes go on `DataTable.Root`, and `hidePagination` belongs to `<DataTable>` alone (leave `DataTable.Pagination` out instead). The parts are `DataTable.Root`, `DataTable.Table`, `DataTable.Head`, `DataTable.Body`, `DataTable.Foot`, `DataTable.Row`, `DataTable.StatusRows`, `DataTable.Pagination` and `DataTable.DragHandle`. `useDataTableContext()` reads the same model from any component among them.

```tsx
import { DataTable, useDataTableContext } from "slotsmith/data-table";
import { columns, people } from "../shared/people";

// Reads the same model as the table, from inside the provider.
function Summary() {
  const { selection, table } = useDataTableContext();
  return (
    <p>
      {selection.length} of {table.getCoreRowModel().rows.length} selected
    </p>
  );
}

// The default layout is Root > Table + Pagination. Here the pagination sits on
// top and a summary line follows the table, all inside the root.
export default function CompoundParts() {
  return (
    <DataTable.Provider data={people} columns={columns} enableRowSelection defaultPagination={{ pageIndex: 0, pageSize: 4 }}>
      <DataTable.Root>
        <DataTable.Pagination />
        <DataTable.Table />
        <Summary />
      </DataTable.Root>
    </DataTable.Provider>
  );
}
```

These are layout parts, not slots: each still renders the slots inside it, so `components` keeps working. The autocomplete, the date picker and the file uploader have their own; see the [autocomplete's compound parts](https://slotsmith.dev/ar/components/autocomplete/guides/compound-parts/) and the [date picker's](https://slotsmith.dev/ar/components/date-picker/guides/compound-parts/).

## Headless hooks

Under every component is a hook with the state and no markup: `useDataTable`, `useAutocomplete`, `useDatePicker` and `useFileUploader`, each exported from its component's entry. `<DataTable>` calls `useDataTable` for you; call it yourself when no slot replacement gets you the markup you need.

```tsx
import { flexRender } from "@tanstack/react-table";
import { useDataTable } from "slotsmith/data-table";
import { columns, people } from "../shared/people";

// No slots, no markup: the hook gives the state and the TanStack instance,
// and the list below is entirely this file's own.
export default function HeadlessHooks() {
  const { table, status } = useDataTable({ data: people, columns, defaultPagination: { pageIndex: 0, pageSize: 3 } });
  if (status === "empty") return <p>No people</p>;
  return (
    <ul>
      {table.getRowModel().rows.map((row) => (
        <li key={row.id}>
          {row.getAllCells().map((cell) => (
            <span key={cell.id}>{flexRender(cell.column.columnDef.cell, cell.getContext())} </span>
          ))}
        </li>
      ))}
    </ul>
  );
}
```

The hook returns the TanStack Table instance, the `status` and the `selection`, and runs row reorder for markup of your own. The data table's [headless hook guide](https://slotsmith.dev/ar/components/data-table/guides/headless-hook/) covers it in full.

## Rules

- **A custom `Row` keeps a stable identity.** Define slot components outside the component that renders the table, or memoize the `components` map. A component written inline in `components={{ Row: (props) => <tr {...props} /> }}` is a new type on every render, so React remounts every row: focus, a running drag and any state inside the row are lost.
- **Spread what an element part receives.** Leaving out props drops the `data-*` state, the handlers or the accessible names.
- **Widget parts get told what is true, not what to render.** A widget part decides its markup from the facts it receives (`checked`, `pageCount`, `direction`) and reports back through the callbacks it is given (`onCheckedChange`, `nextPage`), rather than reaching into the table.
- **Keep the accessible names.** Widget parts receive their `aria-label` from the labels; pass it to the control that takes focus.
