# Column types

> Pick a column type for each column, from prices and percentages to badges, ratings and progress bars, or register your own.

A column's `type` decides how its values look and behave: the number format, the alignment, how it sorts and which filter it offers. Data types format numbers, dates and money. Rich types draw the value as a small widget. When none fits, register a type of your own with `cellTypes`, or draw the cell yourself with [custom cells](https://bonguynvan.github.io/bo-grid/docs/custom-cells/).

Every column below is a built-in type. Sort any of them: each sorts by its plain value, not by the widget.

Example: Rich column types (column-types.svelte)

```svelte
<script lang="ts">
  import { Grid, type ColumnDef } from 'bo-grid';
  import { team } from './team-data';

  // Rich types draw the value as a widget; each still sorts, filters,
  // copies and exports by the plain value.
  const columns: ColumnDef[] = [
    { type: 'avatar', key: 'name', header: 'Member', sub: 'role', width: 150 },
    { type: 'badge', key: 'status', header: 'Status', width: 88, tones: { Active: 'up', Away: 'amber', Offline: 'neutral' } },
    { type: 'progress', key: 'workload', header: 'Workload', min: 0, max: 100, width: 90 },
    { type: 'rating', key: 'rating', header: 'Rating', max: 5, width: 90 },
    { type: 'tags', key: 'skills', header: 'Skills', width: 140 },
    { type: 'link', key: 'email', header: 'Email', href: (r) => `mailto:${r.email}`, flex: 1 },
  ];

  let rows = $state(team());
</script>

<Grid {rows} {columns} height={256} />
```

team-data.ts:

```ts
// A small team list for the docs' rich-cell examples.
export type Member = {
  id: number;
  name: string;
  role: string;
  email: string;
  status: 'Active' | 'Away' | 'Offline';
  workload: number;
  rating: number;
  skills: string[];
};

/** A fresh copy of the team, so one example's edits never reach another. */
export function team(): Member[] {
  return [
    { id: 1, name: 'Ava Chen', role: 'Engineer', email: 'ava@acme.io', status: 'Active', workload: 82, rating: 5, skills: ['TypeScript', 'Svelte'] },
    { id: 2, name: 'Liam Patel', role: 'Designer', email: 'liam@acme.io', status: 'Away', workload: 46, rating: 4, skills: ['Figma', 'CSS'] },
    { id: 3, name: 'Mia Kim', role: 'PM', email: 'mia@acme.io', status: 'Active', workload: 64, rating: 4, skills: ['Roadmaps'] },
    { id: 4, name: 'Noah Garcia', role: 'Analyst', email: 'noah@acme.io', status: 'Offline', workload: 21, rating: 3, skills: ['SQL', 'Python'] },
    { id: 5, name: 'Zoe Nguyen', role: 'Engineer', email: 'zoe@acme.io', status: 'Active', workload: 93, rating: 5, skills: ['Rust', 'Go'] },
    { id: 6, name: 'Ravi Haddad', role: 'Researcher', email: 'ravi@acme.io', status: 'Away', workload: 38, rating: 2, skills: ['Data viz'] },
    { id: 7, name: 'Yuki Rossi', role: 'Writer', email: 'yuki@acme.io', status: 'Active', workload: 57, rating: 4, skills: ['Docs', 'CSS'] },
  ];
}
```

## Data types

| Type | Value | Shows |
| --- | --- | --- |
| `text` | string | Text; `sub` names a field shown as a second, dimmer line |
| `price` | number | A price with two decimals, in the column's locale |
| `percent` | number | A signed percentage (`+1.20%`), coloured up or down |
| `volume` | number | A compact count: 1.2K, 3.4M, 1.1B, 2.0T |
| `number` | number | A number with `decimals` places (default 2) |
| `currency` | number | Money in `currency` (default USD), through `Intl.NumberFormat` |
| `date` | epoch ms (or a `Date`) | A date; `dateStyle` is `'short'` or `'medium'` (default) |
| `relative` | epoch ms | A relative time: "3 hours ago" |
| `heatmap` | number | The value on a colour ramp from `min` to `max` |
| `sparkline` | candles | A small chart drawn from the row field named by `sparkKey` |

## Rich types

| Type | Value | Shows |
| --- | --- | --- |
| `avatar` | display name | Initials in a circle, with an optional `sub` line |
| `badge` | string | A pill; `tones` maps values to `up`, `down`, `amber`, `info` or `neutral` |
| `progress` | number | A bar from `min` to `max` |
| `rating` | number | Stars out of `max` |
| `tags` | string array, or a comma-separated string | Chips |
| `boolean` | boolean | `trueLabel` or `falseLabel` |
| `link` | text | A link to `href(row)`; `newTab` opens a new tab |
| `custom` | anything | Whatever the grid's `cell` snippet draws |

```ts
const columns: ColumnDef[] = [
  { type: 'avatar', key: 'name', header: 'Member', sub: 'role' },
  { type: 'badge', key: 'status', header: 'Status', tones: { Active: 'up', Away: 'amber', Offline: 'neutral' } },
  { type: 'progress', key: 'workload', header: 'Workload', min: 0, max: 100 },
  { type: 'rating', key: 'rating', header: 'Rating', max: 5 },
  { type: 'tags', key: 'skills', header: 'Skills' },
  { type: 'boolean', key: 'remote', header: 'Where', trueLabel: 'Remote', falseLabel: 'Office' },
  { type: 'link', key: 'email', header: 'Email', href: (r) => `mailto:${r.email}` },
];
```

- `link` blocks unsafe hrefs (`javascript:` and `data:` URLs).
- Rich types take their colours from the theme, so they follow light and dark themes with no extra code.
- A widget is only how a value is drawn: sorting, filtering, copying and export use the value itself.

## Registered cell types

When the same kind of column appears in several grids, such as a country, a money amount or an order status, define it once and give it a name. `cellTypes` maps a name to column defaults, and a column that says `cellType: 'name'` gets them under its own fields:

```svelte
<script lang="ts">
  import { Grid, type CellTypeDef, type ColumnDef } from 'bo-grid';
  import CountryCell from './CountryCell.svelte';

  const cellTypes: Record<string, CellTypeDef> = {
    country: { extends: 'text', component: CountryCell, filter: 'set', width: 140 },
    money: { extends: 'number', decimals: 0, format: (v) => `$${Number(v).toLocaleString()}` },
  };
  const columns: ColumnDef[] = [
    { cellType: 'country', key: 'country', header: 'Country' },
    { cellType: 'money', key: 'rate', header: 'Rate', width: 96 }, // its own width wins
  ];
</script>

<Grid {rows} {columns} {cellTypes} height={560} />
```

- **`extends`** names the built-in type the new one builds on: its formatting, sorting, filtering, alignment, aggregation and export. It defaults to `custom` when the entry draws the cell itself (`component` or `render`), and to `text` otherwise.
- An entry can set any column field: `format`, `compare`, `align`, `filter`, `width`, `cellClass`, `tooltip`, `editable`, and type options such as `decimals`.
- An unknown `cellType` renders as plain text rather than failing.
- Outside the grid, `resolveColumns(columns, cellTypes)` returns the same resolved columns, for `toCSV`, `printTable` or `pivot`.

The **Team** example registers a `country` type with a flag component: see [Custom cells](https://bonguynvan.github.io/bo-grid/docs/custom-cells/).

## Reference

- `cellTypes`: `Record<string, CellTypeDef>` (default `undefined`) — Reusable column types, by name. A column with `cellType: 'name'` gets that entry's defaults (format, compare, align, filter, width, a `component` renderer…) under its own fields, and the built-in type named by `extends` for sorting, filtering and export.

  ```svelte
  <Grid {rows} {columns} height={480}
    cellTypes={{ qty: { extends: 'number', decimals: 0, width: 80, format: (v) => Number(v).toLocaleString() } }} />
  ```

- `defaultColumn`: `DefaultColumn` (default `undefined`) — Settings applied under every column — its own fields (and a registered `cellType`) win.

  ```svelte
  <Grid {rows} {columns} height={480} defaultColumn={{ minWidth: 72, sortable: false }} />
  ```

- `locale`: `string` (default `undefined`) — BCP 47 locale for the grid's own number formatting (aggregation bar, pager row count), and the default for each column's `locale`. Unset, the runtime's locale.

- `format`: `(value: unknown, row?: GridRow) => string` — Custom display formatter, overriding the built-in type formatter. Applies to display, tooltip, copy and (formatted) export. `row` is absent for aggregate cells.

  ```ts
  { type: 'number', key: 'qty', header: 'Qty', format: (v) => Number(v).toLocaleString('vi-VN') }
  ```

- `component`: `Component<CellTypeProps>` — Svelte component that draws this cell (receives `value`, `row`, `column` and the formatted `text`). Display only, like `render`. Usually supplied by a registered `cellTypes` entry, but any column can set it. It updates in place as its props change; under `api.patchRows`, `row` is a fresh view of the patched row on each patch (same fields, new identity), so read it reactively (`$derived`, markup) rather than once at mount.

  ```ts
  { type: 'text', key: 'country', header: 'Country', component: CountryCell }
  ```

- `locale`: `string` (default `undefined`) — BCP 47 locale for this column's built-in formatting (`price`, `date`, `currency`, `relative`). Unset, the grid's `locale`, then `en-US`.

Every type and the options it adds are listed in the [Props reference](https://bonguynvan.github.io/bo-grid/docs/reference/#column-types).

---
Page: https://bonguynvan.github.io/bo-grid/docs/column-types/ · All pages: https://bonguynvan.github.io/bo-grid/llms.txt
