---
url: "https://vueda.dev/v3/guides/customize-list-column-rendering.md"
title: "Customize List Column Rendering"
---

# Customize List Column Rendering

This guide shows how to change the component that renders a column's cells in [ViewList](/v3/reference/api/vue/ViewList.md), change the props it receives, or write your own. Columns you leave alone keep their defaults.

Each cell renders through a [Column Adapter](/v3/reference/glossary.md#column-adapter). [Contract-First Dynamic UI](/v3/core-concepts/contract-first-dynamic-ui.md#configuration-precedence) describes how model config and view props combine in general. This guide gives the order for list columns.

To link a row to its own read or update view, use a [Row Link](/v3/reference/glossary.md#row-link). [Link List Rows to Read and Update Views](/v3/guides/link-list-rows-to-detail-views.md) describes it, including which adapters it wraps.

## Before You Start

- The model has a [Canonical Registration](/v3/reference/glossary.md#canonical-registration) with a working `list` view ([Create a CRUD Surface](/v3/guides/create-crud-surface.md)). Check that the list renders before you add overrides.
- You know the field name of each column you want to change. Overrides are keyed by field name.

## How Default Columns Are Chosen

VUEDA picks each column's default adapter in [utils/columnMappings.columnMappings](/v3/reference/api/js/utils/columnMappings.md#columnMappings). It looks up the field's [`typeSerializer`](/v3/reference/api/js/stores/storeModelInfo/types/FieldInfo.md#typeSerializer), then its [`typeModel`](/v3/reference/api/js/stores/storeModelInfo/types/FieldInfo.md#typeModel):

| Serializer type          | Model type                     | Adapter                              | Default props                                              |
| ------------------------ | ------------------------------ | ------------------------------------ | ---------------------------------------------------------- |
| `BooleanField`           | `BooleanField`                 | [ColumnBoolean](/v3/reference/api/vue/ColumnBoolean.md)   |                                                            |
| `NullBooleanField`       | `NullBooleanField`             | `ColumnBoolean`                      |                                                            |
| `DateField`              | `DateField`                    | [ColumnDateTime](/v3/reference/api/vue/ColumnDateTime.md)  | `{ showTime: false }`                                      |
| `DateTimeField`          | `DateTimeField`                | `ColumnDateTime`                     | `{ showTime: true }`                                       |
| `TimeField`              | `TimeField`                    | `ColumnDateTime`                     | `{ format: "t", showRelative: false, showTooltip: false }` |
| `DurationField`          | `DurationField`                | [ColumnDuration](/v3/reference/api/vue/ColumnDuration.md)  |                                                            |
| `DurationSecondsField`   | `DurationField`                | `ColumnDuration`                     |                                                            |
| `FileField`              | `FileField`                    | [ColumnFile](/v3/reference/api/vue/ColumnFile.md)      |                                                            |
| `ImageField`             | `ImageField`                   | `ColumnFile`                         |                                                            |
| `JSONField`              | `JSONField`                    | [ColumnJson](/v3/reference/api/vue/ColumnJson.md)      |                                                            |
| `PrimaryKeyRelatedField` | `ForeignKey` / `OneToOneField` | [ColumnModelLink](/v3/reference/api/vue/ColumnModelLink.md) | `{ view: "read" }`                                         |

Every other type renders through [ColumnText](/v3/reference/api/vue/ColumnText.md). That includes to-many relations and `SlugRelatedField`.

The adapters render as follows:

- `ColumnText` renders the cell value as text. It renders nothing for `null` or `undefined`. It renders an object or array as compact `JSON`, cut at 200 characters.
- `ColumnBoolean` renders [BooleanDisplay](/v3/reference/api/vue/BooleanDisplay.md): "Yes" or "No".
- `ColumnDateTime` renders [DateTimeDisplay](/v3/reference/api/vue/DateTimeDisplay.md).
- `ColumnDuration` renders [DurationDisplay](/v3/reference/api/vue/DurationDisplay.md), which names the units.
- `ColumnFile` renders [FileDisplay](/v3/reference/api/vue/FileDisplay.md): a link to the file, labelled with the file name.
- `ColumnJson` renders [JsonDisplay](/v3/reference/api/vue/JsonDisplay.md): compact `JSON` on one line, cut at `maxLength` (200 characters by default).
- `ColumnModelLink` renders a foreign key as a link to the related row's read view, through [LinkModelView](/v3/reference/api/vue/LinkModelView.md).

`BooleanDisplay`, `DateTimeDisplay`, `DurationDisplay`, `FileDisplay`, and `JsonDisplay` render a dash for an empty value.

## Choose Where to Override

| To change                                      | Use                                               |
| ---------------------------------------------- | ------------------------------------------------- |
| One column of a model, in every list           | [Model config](/v3/guides/customize-list-column-rendering.md#set-an-override-in-model-config)  |
| One column in a `ViewList` you render yourself | [`ViewList` props](/v3/guides/customize-list-column-rendering.md#pass-overrides-as-props)      |
| The markup of one column in one rendered list  | [A slot](/v3/guides/customize-list-column-rendering.md#replace-a-column-with-a-slot)           |
| Every column of one type, in every model       | [A type mapping](/v3/guides/customize-list-column-rendering.md#map-a-field-type-to-an-adapter) |

For one column, the first of these that is set picks the adapter:

1. A `field(<name>)` slot on `ViewList`.
2. The `ViewList` `columnComponents` prop.
3. The model config's `columnComponents`.
4. The type mapping, else `ColumnText`.

Props combine separately, as [Change Only an Adapter's Props](/v3/guides/customize-list-column-rendering.md#change-only-an-adapter-s-props) describes.

## Set an Override in Model Config

1. Get the store with [storeModelConfig](/v3/reference/api/js/stores/storeModelConfig/functions/storeModelConfig.md).
2. Call [`setConfig`](/v3/reference/api/js/stores/storeModelConfig/methods/setConfig.md) when the application starts. [Register Overrides with `setConfig`](/v3/guides/configure-crud-views.md#register-overrides-with-setconfig) describes its arguments.
3. In the `list` layer, set [`columnComponents`](/v3/reference/api/js/stores/storeModelConfig/types/OverridingModelConfig.md#columnComponents) to choose adapters. Set [`columnProps`](/v3/reference/api/js/stores/storeModelConfig/types/OverridingModelConfig.md#columnProps) to add props. Key both by field name.

```js
import ColumnStatusBadge from "./ColumnStatusBadge.vue";
import { storeModelConfig } from "@vueda/stores/storeModelConfig.js";

storeModelConfig().setConfig({ app: "myapp", model: "widget" }, null, {
    list: {
        columnComponents: {
            status: () => ColumnStatusBadge,
            release_date: "ColumnDateTime",
        },
        columnProps: {
            release_date: { showTime: false, format: "yyyy-LL-dd" },
        },
    },
});
```

A `columnComponents` entry takes one of three values:

- **A function that returns a component**, such as `() => ColumnStatusBadge`. VUEDA calls it with no arguments. Use this form for your own components. The store keeps its values in reactive state and merges plain objects between layers; the function keeps the component out of both.
- **The name of a built-in adapter** in [utils/columnLookups.availableColumns](/v3/reference/api/js/utils/columnLookups.md#availableColumns): `"ColumnText"`, `"ColumnBoolean"`, `"ColumnDateTime"`, `"ColumnDuration"`, `"ColumnFile"`, `"ColumnJson"`, or `"ColumnModelLink"`. Use a name to give a column a built-in adapter that its type does not map to.
- **A component object.** Use this form in the `columnComponents` prop. In model config, wrap the component in a function.

The model-wide layer also applies to the list view. When both layers set `columnComponents`, the list gets the entries of both. For a column that both layers name, the `list` layer wins. `columnProps` merges each column's props across the layers, key by key.

## Pass Overrides as Props

When you render `ViewList` yourself, pass [`columnComponents`](/v3/reference/api/vue/ViewList.md#prop-columnComponents) and [`columnProps`](/v3/reference/api/vue/ViewList.md#prop-columnProps). They take the same values as the model config keys.

```vue
<template>
    <ViewList app="myapp" model="widget" :column-components="{ category: 'ColumnText' }" />
</template>
```

For the same column, a `columnComponents` prop entry wins over model config. It wins even when it names no adapter. The column then shows the error described in [Troubleshooting](/v3/guides/customize-list-column-rendering.md#troubleshooting), even if model config has a valid entry for it.

## Change Only an Adapter's Props

To keep a column's adapter and change how it renders, set only `columnProps`. The adapter receives the type mapping's default props first. The model config's `columnProps` for the column come next, then the `columnProps` prop. A later source wins for the same key.

This hides the time on one datetime column:

```js
storeModelConfig().setConfig({ app: "myapp", model: "widget" }, null, {
    list: { columnProps: { updated_at: { showTime: false } } },
});
```

Each built-in adapter's generated page lists its props. The ones you are most likely to set:

- `ColumnDateTime`: `format` (default `"absolute"`, or a Luxon format string), `showTime`, `showRelative`, `showTooltip`, `tooltipFormat`.
- `ColumnBoolean`: `trueLabel`, `falseLabel`.
- `ColumnDuration`: `format` (default `"long"`).
- `ColumnJson`: `maxLength`.
- `ColumnModelLink`: `view` (default `"read"`), `app` and `model`, `label`, `button`.

## Link a Foreign Key Column

A `ForeignKey` or `OneToOneField` column renders through `ColumnModelLink` with no configuration. It reads the related model from the field's [`appLabel`](/v3/reference/api/js/stores/storeModelInfo/types/FieldInfo.md#appLabel) and [`model`](/v3/reference/api/js/stores/storeModelInfo/types/FieldInfo.md#model). Model info sends both for writable and read-only relation fields.

- **Related pk:** a scalar cell value is the pk. An object value uses its `id`, else its `pk`.
- **Link text:** the `label` prop, else the cell's `formatted` value when it is text or a number. For a scalar foreign key, the text is the pk. For an expanded foreign key, the text is the related object's `formatted_name`, else its `name`, `id`, or `pk`.
- **No link:** when no related model or no pk resolves, or the value is an array, the cell renders the text without a link.

When the field carries no related model, supply one through `columnProps`:

```js
storeModelConfig().setConfig({ app: "myapp", model: "widget" }, null, {
    list: { columnProps: { category: { app: "myapp", model: "widgetcategory" } } },
});
```

The link controls navigation in the UI only. When the user follows it, the target view's route guard and the server still check the user's access.

## Replace a Column with a Slot

A [`field(<name>)`](/v3/reference/api/vue/ViewList/slots.md) slot on `ViewList` replaces the column's content and wins over every adapter. It receives the same props as an adapter ([Write a Column Adapter](/v3/guides/customize-list-column-rendering.md#write-a-column-adapter)).

```vue
<template>
    <ViewList app="myapp" model="widget">
        <template #[`field(sku)`]="{ value, obj }">
            <code>{{ value }}</code>
            <span v-if="obj.is_active" class="ml-2 text-green-600">active</span>
        </template>
    </ViewList>
</template>
```

[ObjectsGrid](/v3/reference/api/vue/ObjectsGrid.md) renders the slot in both the table cell and the card cell. If you wrap `ViewList` in your own component, forward the slot to `ViewList`.

## Map a Field Type to an Adapter

To change the default for a type in every model, call [mergeColumnMappings](/v3/reference/api/js/utils/columnMappings/functions/mergeColumnMappings.md) in your client entry, before the app mounts. It deep-merges your entries into `columnMappings`. The outer key is the `typeSerializer`, and the inner key is the `typeModel`.

This shows times as hours and minutes in every list:

```js
import { mergeColumnMappings } from "@vueda/utils/columnMappings.js";

mergeColumnMappings({
    TimeField: {
        TimeField: { columnProps: { format: "HH:mm" } },
    },
});
```

An entry's [`column`](/v3/reference/api/js/utils/columnMappings/types/ColumnMappingEntry.md#column) names an adapter in `availableColumns`, and [`columnProps`](/v3/reference/api/js/utils/columnMappings/types/ColumnMappingEntry.md#columnProps) holds its default props. The entry marked [`default`](/v3/reference/api/js/utils/columnMappings/types/ColumnMappingEntry.md#default) applies when `typeModel` is empty. A `column` that names no adapter shows the column error described in [Troubleshooting](/v3/guides/customize-list-column-rendering.md#troubleshooting) on every column of that type, unless an override sets the column's adapter.

## Write a Column Adapter

An adapter is a Vue component. `ViewList` passes it the cell's slot props, with the column's resolved `columnProps` on top. The table cell and the card cell pass the same props:

- [`value`](/v3/reference/api/vue/ObjectsGridBodyCell.md#slot-value-param-value): the field's value in the row.
- [`formatted`](/v3/reference/api/vue/ObjectsGridBodyCell.md#slot-value-param-formatted): in a `ViewList` cell, the same value as `value`.
- [`obj`](/v3/reference/api/vue/ObjectsGridBodyCell.md#slot-value-param-obj): the row object. [`relatedObj`](/v3/reference/api/vue/ObjectsGridBodyCell.md#slot-value-param-relatedObj) and [`calculatedObj`](/v3/reference/api/vue/ObjectsGridBodyCell.md#slot-value-param-calculatedObj) hold the row's related and calculated objects.
- [`pk`](/v3/reference/api/vue/ObjectsGridBodyCell.md#slot-value-param-pk): the row's primary key. A foreign key's target pk is in `value`.
- [`field`](/v3/reference/api/vue/ObjectsGridBodyCell.md#slot-value-param-field): the column's field descriptor.
- `rowIndex`, `columnIndex`, `rowCount`, `columnCount`, `isTableLayout`, and `isCardLayout`: the cell's position and the current layout.
- `pkKey`, `modelInfo`, and `modelConfig`: the model's pk field name, its [Model Info](/v3/reference/glossary.md#model-info), and its [Model Config](/v3/reference/glossary.md#model-config).

Declare only the props you use. Set `inheritAttrs: false` so the others do not become attributes on your root element. The built-in adapters do both.

```vue
<script setup>
defineOptions({ inheritAttrs: false });
defineProps({
    value: { type: Number, default: null },
});
</script>

<template>
    <span v-if="value != null" :class="value >= 0 ? 'text-green-600' : 'text-red-600'">{{ value }}</span>
</template>
```

Register it through a function in model config, or pass it in the `columnComponents` prop.

## Check the Result

- Each changed column renders its adapter in the table layout. Below the list's [`tableBreakpoint`](/v3/reference/api/vue/ViewList.md#prop-tableBreakpoint), it renders the adapter in the card layout too.
- A foreign key column links to the related row, and its URL carries the related row's pk. An empty value renders without a link.
- Columns you did not change render as before.
- No column error shows above the list.

## Troubleshooting

**The list shows "There was an error while rendering the list columns."** A `columnComponents` entry or a type mapping's `column` does not name an adapter, or a function returned nothing. The message names the column, such as `No column component named "Nope" for column "status"`, and adds `in the type mapping` when the mapping is the cause. That column's cells render empty, and the other columns render normally. Check the name against `availableColumns`, or pass your own component through a function. The error shows even when a `field(<name>)` slot fills the column. A list you build on [useViewList](/v3/reference/api/js/use/useViewList/functions/useViewList.md) gets these errors in [`columnErrors`](/v3/reference/api/js/use/useViewList/types/ViewListListGroup.md#columnErrors).

**The override has no effect.** Check that the key matches the field name exactly. A `field(<name>)` slot wins over both override maps, and a `columnComponents` prop entry wins over model config. A `setConfig` call made after the list has built its config does not reach that list, so call `setConfig` when the application starts.

**A foreign key column renders text without a link.** The value is empty or an array, or no related model resolved. Check the field's `app_label` and `model` in the [model info response](/v3/reference/api/rest/schemas/ModelInfoField.md). Or supply `app` and `model` through `columnProps`.

**A to-many relation renders as `[2, 1]`.** A to-many relation's list value is an array of pks, and `ColumnText` renders it as `JSON`. To render linked items, expand the relation so each item carries a label, and write an adapter for it.

**Attributes you did not set appear on your adapter's root element.** Add `defineOptions({ inheritAttrs: false })`.
