---
title: "Custom Table"
slug: "custom-table"
category: "Widgets"
reactComponent: "ArcWidgetTable"
reactTier: "premium"
reactSince: "0.1.0-alpha.5"
reactExample: "table.tsx"
locale: "es"
hosts: "ninox"
---

# Custom Table

## AI Defaults (read first)

- **Column widths**: Every column MUST have explicit `width`. At least one column MUST use `width: "fraction"`.
- **Height**: Use `height: "100%"` (not `calc()`). Especially when `embedded: true`.
- **uniqueListId**: Required, use `"my-table-" + Nr`.
- **tableId**: Required (e.g. from `tableId("Tabellenname")`).
- **recordId**: Always raw `Nr`, NEVER `number(Nr)`.
- **Empty state**: Use `emptyTable: { title: "...", value: "..." }`, do NOT wrap table in if/else.
- **theme.uniqueId**: Must match `uniqueListId`.
- **Columns value**: Each column needs `recordId: Nr` inside the mapping.
- **Row actions**: Use `actions` array with `type: "popup"` or `type: "update"`, recordId = `Nr`.
- **Sort**: Client-side only; `header.columns[n].sort: { enabled: true, type: "text"|"number"|"date" }`; optional `columns[n].sort.value` for sort key when `value` is not plain text. Optional `header.sortIcons: { color, activeColor, hoverColor }` for caret colors (defaults: muted gray / black active / stronger on header hover).
- **Hover**: `actions: [{ type: "hover", fontColor, backgroundColor, ... }]` on table root, row, or column; `type: "hover"` is not passed to click handlers. Legacy `rowHoverAction` still supported.
- **Borders**: Live under `styles.borders`. Default is a rounded frame + row lines, **no vertical cell lines**. Hide the frame with `styles: { borders: { outer: false } }`. Opt into a grid with `columns: true`. Set `styles.borderColor` for dark backgrounds; a line may be `{ color, width }` instead of a boolean.
- **styles hierarchy**: Same `styles` bag on table, header, row, and column — prefer `{ backgroundColor, fontColor, … }` over a CSS string. Table `styles.backgroundColor` is the default surface. A row or column overrides with its own `styles.backgroundColor`. Header uses `header.backgroundColor` / `header.styles`. Do not invent extra props like `rowBackgroundColor`.

# Custom Table

El widget **Custom Table** te permite representar tablas dinámicas e interactivas directamente en tu superficie de Ninox, adaptadas a tus necesidades. Es ideal para mostrar datos estructurados de forma clara, darles formato individual y vincularlos con acciones (p. ej. para editar, eliminar, abrir registros o llamadas a la API).

<iframe src="https://www.youtube.com/embed/gnP9o5NCefk?iv_load_policy=3&rel=0&modestbranding=1&playsinline=1&autoplay=1&mute=1&loop=1&playlist=gnP9o5NCefk" data-thumbnail="Medium Quality" frameborder="0" allow="presentation; fullscreen; accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture"></iframe>## Ejemplo:

![](https://framerusercontent.com/images/LW07tfdMxAVkiyhTZ9GOJlmHFws.png)

## Código de aplicación completo

A continuación ves un código de aplicación de ejemplo que define la base de tu **Custom Table**. Como el código puede llegar a ser muy extenso según el caso de uso, te guiamos paso a paso **desde una base sencilla** hasta **variantes más complejas**.

La estructura se divide en dos áreas centrales:

- `**data**` – define el contenido y la estructura de tu tabla (filas y columnas)
- `**arcCustomTable()**` – es la función global que renderiza el widget. A ella le pasas los `data`


```javascript
let current := this;
let projectList := Artikel;
let data := {
        uniqueListId: "beispiel" + Nr,
        tableId: "",
        height: "500px",
        minHeight: "",
        groupedBy: "",
        groupsCollapsed: false,
        theme: arcCustomThemeCleanWhite({
                    uniqueId: "beispiel"
                }),
        embedded: false,
        rowHoverAction: {
            hoverActive: true,
            backgroundColor: "",
            fontColor: ""
        },
        header: {
            showHeader: true,
            height: "",
            fontColor: "",
            backgroundColor: "",
            fontSize: "",
            columns: [{
                    title: "Prio",
                    width: ""
                }]
        },
        emptyTable: {
            title: "Keine Ergebnisse",
            backgroundColor: "",
            value: ""
        },
        scrollBar: {
            showScrollBar: false,
            height: "",
            backgroundColor: "",
            handle: {
                height: "",
                borderRadius: "",
                backgroundColor: ""
            }
        },
        table: projectList.[{
                recordId: Nr,
                rowColor: "",
                rowHeight: "auto",
                rowPaddingY: "10px",
                groupRowColor: "",
                groupRowSettings: {
                    height: "",
                },
                columns: [{
                        field: "",
                        title: "",
                        value: "",
                        color: "",
                        backgroundColor: "",
                        width: "",
                        align: "left",
                        paddingX: "",
                        paddingY: "",
                        groupExpandAction: false,
                        groupValue: arcCustomIcon({
                                name: "caret-down"
                            }),
                        groupByValue: "",
                        actions: [{
                                type: "popup",
                                showPopupButton: false,
                                recordId: Nr
                            }]
                    }]
            }],
        footer: {
            showFooter: false,
            showActionButton: true,
            backgroundColor: "",
            actionButtonTitle: "",
            leftSideContent: "",
            rightSideContent: "Gesamt: " + cnt(projectList) + " Projekte"
        }
    };
arcCustomTable(data)
```

### uniqueListId

El parámetro `**uniqueListId**` es la **denominación individual de tu tabla**. Se encarga de que tu tabla se identifique de forma única internamente, especialmente cuando se muestran **varias tablas a la vez en una página**.

**¿Por qué es importante?**

- Los ajustes de estilo (p. ej. colores, tipografías, efectos hover) se aplican de forma específica solo a la tabla con la `uniqueListId` indicada.
- Con varias tablas en una vista, sin esto pueden surgir **conflictos de clases CSS o de estados**.

**✅ Buena práctica:**

Usa **nombres descriptivos y únicos** – idealmente en camelCase o con guiones bajos.


```javascript
uniqueListId: "Projektliste Offen", // Text
```

### tableId

Con `**tableId**` indicas el **ID interno de la tabla en Ninox** al que hace referencia el widget. Este ID se necesita, por ejemplo, para:

- crear nuevos registros mediante el **botón «+»** en el footer (`create`)
- poder abrir o editar registros existentes (`popup`, `openFullscreen`, `update`)
- referenciar correctamente las acciones a los registros de esta tabla


```javascript
tableId: "AB", // Die ID als Textform
tableId: tableId("Kontakte"), // Ninox Funktion zum Herausfinden der Table ID deiner darzustellenden Tabelle
```

### height

Con `**height**` determinas la **altura del widget de tabla** en **píxeles**. Así puedes adaptar la representación de forma flexible al layout de tu página, sin importar cuántos datos contenga.

**💡  Valores**

- El valor se indica en píxeles, p. ej. `"300px"`, `"500px"` o `"800px"`.
- `**"auto"**`: la altura se adapta automáticamente al contenido. Perfecto para contenidos dinámicos, pero ten en cuenta posibles saltos en el layout.
- Combinado con `maxHeight` puedes limitar la altura automática.


```javascript
height: "500px", // Pixel Werte
```

### minWidth

Con `**minWidth**` fijas el **ancho mínimo** de tu tabla, independientemente de cuántas columnas o datos contenga. Así te aseguras de que la tabla no se muestre demasiado estrecha, por ejemplo en contenedores estrechos, pestañas o vistas incrustadas.

🔍 **Formato**

- El valor se pasa como **texto con unidad**, p. ej. `"600px"`
- Sin especificarlo, la tabla puede colapsar y volverse estrecha si hay poco contenido, lo que puede afectar el layout.

💡 **Nota:**

`minWidth` afecta al contenedor exterior de la tabla, no a los anchos de columna internos. Para las columnas existen opciones separadas como `width` y `minWidth` directamente en el bloque `columns`.

### groupedBy

Con `**groupedBy**` defines según qué **campo (clave de columna o **`**field**`**)** debe agruparse tu tabla. Los grupos se muestran visualmente como **filas separadas con formato propio** — las entradas subyacentes se pueden expandir y contraer.

🔍 **Comportamiento**

- Aquí indicas el **nombre de columna (**`**field**`**)** definido en tu bloque `columns`.
- Si el campo está vacío (`"`) o falta por completo, **no se produce agrupación**.
- La agrupación también puede basarse en valores complejos (p. ej. `Typ`, `Kategorie`, `Verantwortlicher`, etc.).
- Los grupos se pueden **expandir y contraer** dinámicamente haciendo clic en la fila de grupo; este estado se **guarda localmente**.


```javascript
groupedBy: "", // default: keine Gruppierung ausgewählt
groupedby: "Status", // Text des Feldtitels, nach dem gruppiert werden soll
groupedBy: Gruppierung, // Ninox Feld, das angesprochen wird. In diesem Fall: Ein Auswahlfeld, in dem die Optionen den Titel verschiedener Ninox-felder tragen, nach denen gruppiert werden kann. (z.B. Status, Mitarbeiter, Aufgabe)
```

<iframe src="https://www.youtube.com/embed/wuHZMD3Ym5Q?iv_load_policy=3&rel=0&modestbranding=1&playsinline=1&autoplay=1&mute=1" data-thumbnail="Medium Quality" frameborder="0" allow="presentation; fullscreen; accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture"></iframe>### groupsCollapsed

Con `**groupsCollapsed**` defines si las filas agrupadas se muestran **contraídas** o **expandidas** por defecto. El encabezado de grupo (groupRow) siempre es visible; solo las entradas asociadas (valueRows) se muestran u ocultan según sea necesario.

**Este parámetro solo actúa cuando **`**groupedBy**`** está establecido.**

`**true**` = los grupos están **contraídos** al cargar (vista compacta).
`**false**` = los grupos están **completamente abiertos** al cargar.

**El estado (expandido/contraído) se guarda localmente**, es decir, al volver a abrir, el widget recuerda el último estado por usuario.

**Valor por defecto: **`**false**`** (los grupos están abiertos)** — si no se indica, la tabla muestra todas las entradas de grupo por completo.


```javascript
groupscollapsed: "", // default: false
groupsCollapsed: true,
```

### embedded

Con `**embedded**` activas el **modo incrustado** de la tabla. En este modo, la tabla se adapta de forma óptima a las estructuras de layout existentes, p. ej. en un contenedor, una pestaña o un área de UI flexible.

**Valor por defecto: **`**false**` — entonces la tabla ocupa su propio espacio fijo en la página.

**Si se establece **`**embedded: true**`**, el widget se posiciona de forma relativa en el contenedor en lugar de absoluta** y adopta su ancho y, en su caso, otras indicaciones de estilo. Combinado con parámetros como `height` y `minWidth`, esto proporciona un aspecto limpio e integrado.

**`**embedded**` es especialmente útil en dashboards, vistas de pestañas o ventanas modales.**


```javascript
embedded: false,
embedded: true,
embedded: "", // default: false
```

### styles.borders

Los bordes y las líneas de cuadrícula viven en **`styles`** — junto con el fondo, el radio y el color de línea.

Por defecto: marco exterior redondeado, líneas de fila finas, **sin líneas verticales de celda**.

Cada campo de línea es `true` / `false` **o** `{ color, width }`.

| Prop | Default | Significado |
|---|---|---|
| `styles.borderColor` | `#e4e4e7` | Color para todas las líneas activadas |
| `styles.borderRadius` | `12px` | Radio de esquina (solo si `outer` está activo) |
| `styles.borders.outer` | `true` | Marco exterior. `false` = a ras en el layout |
| `styles.borders.columns` | `false` | Líneas verticales |
| `styles.borders.rows` | `true` | Líneas horizontales |
| `styles.borders.color` / `width` | — | Línea común, sobrescribe `borderColor` / grosor por defecto |

```javascript
styles: {
  borderRadius: "12px",
  borderColor: "#e4e4e7",
  borders: {
    outer: true,
    columns: false,
    rows: true
  }
}

// In einem Layout / Card:
styles: { borders: { outer: false } }

// Dunkler Hintergrund — Fläche auf styles, Kopf auf header:
styles: {
  backgroundColor: "#2a2822",
  fontColor: "#f5f0e6",
  borderColor: "#3f3c34"
},
header: {
  backgroundColor: "#1c1b16",
  fontColor: "#f5f0e6"
}

// Klassisches Gitter:
styles: { borders: { columns: true } }
```

Variantes ya preparadas (Default, Card, Gitter, flush, Dark): [Custom Table Look](/documentation/custom-table-look).

### rowHoverAction

Con el bloque `**rowHoverAction**` determinas si el **comportamiento hover en las filas de la tabla** debe estar activo y, en caso afirmativo, **qué colores** se usan al pasar el ratón por encima.

- `**hoverActive: true**` activa el efecto; de otro modo, la fila permanece sin cambios al hacer hover.
- `**backgroundColor**` define el **color de fondo** de la fila en el estado hover,
- `**fontColor**` establece el **color de texto** durante el hover.

**El valor por defecto de **`**hoverActive**`** es **`**true**` si el bloque está definido. Sin `rowHoverAction`, el comportamiento es neutro: sin efectos hover.

**Consejo: usa colores discretos** para mantener el efecto sutil y agradable para la experiencia de usuario.


```javascript
rowHoverAction: { hoverActive: true, // wenn hovern an sein soll, und false, wenn nicht
                  backgroundColor: "#9ca4a9", // default: #f4f6ff
                  fontColor: "#fff" } // default: #000
```

**Recomendación:** para proyectos nuevos usa `actions: [{ type: "hover", ... }]` (ver más abajo) — `rowHoverAction` se mantiene por compatibilidad con versiones anteriores.

### actions con type: "hover"

Estilo hover y acciones hover opcionales a través del mismo array `actions` que `update` / `popup`. Una entrada con `**type: "hover"**` **no** se pasa a las acciones de clic, sino que solo la evalúa el widget.

**Orden de fusión:** `rowHoverAction` (legacy) → `actions` en la raíz de la tabla → `row.actions` → `columns[n].actions`.

| Sub-Prop | Significado |
|---|---|
| `enabled` | `false` = sin resaltado hover para esta fila/celda. |
| `backgroundColor` | Color de fondo en hover. |
| `fontColor` | Color de texto en hover. |
| `styles` | CSS adicional (string) para la celda cuando la fila está en hover. |
| `actions` | Opcional: en **mouseenter** hacia el manejador de acciones (p. ej. `update`). |
| `leaveActions` | Opcional: en **mouseleave**. |

**A nivel de fila:** `actions: [{ type: 'hover', backgroundColor: '#eff6ff', fontColor: '#0f172a' }]` en el objeto de fila.

**A nivel de columna:** p. ej. `{ type: 'hover', enabled: false }` para excluir una columna individual del resaltado hover.

**`row.actions`** (sin `hover`) — las entradas adicionales `update`/`popup` se aplican a **toda la fila** (al hacer clic en una celda se ejecutan juntas las acciones de fila y de celda).

### header

Con el bloque ***header*** controlas la representación de tu **fila de encabezado de tabla** (títulos de columna).

**Comportamiento por defecto:** si no se indica `showHeader`, la fila de encabezado se **muestra**. Sin más ajustes, se aplican valores por defecto para tamaño, color y fondo.

Consejo: si estableces `showHeader: false`, el significado de los datos debería seguir siendo claro (posición, esquema de colores de las columnas).

#### showHeader

Muestra u oculta toda la fila de encabezado.

```javascript
header: {
  showHeader: false, // default: true
}
```

#### height

Altura de la fila de encabezado (p. ej. `"48px"` o `"auto"`).

```javascript
header: {
  height: "70px", // default: 36px
}
```

#### fontColor

Color de fuente de los títulos de columna.

```javascript
header: {
  fontColor: "#fff", // default: #000
}
```

#### fontSize

Tamaño de fuente de los títulos.

```javascript
header: {
  fontSize: "17px", // default: 12px
}
```

#### backgroundColor

Fondo de la fila de encabezado.

```javascript
header: {
  backgroundColor: "#3a4a54", // default: #f8f9fc
}
```

#### styles

String CSS para dar estilo libre al contenedor del encabezado (sobrescribe `fontColor`, `backgroundColor`, etc.).

```javascript
header: {
  styles: "border-bottom: 1px solid #eee;",
}
```

Ejemplo completo:

```javascript
header: {
  showHeader: false, // default: true
  height: "70px", // default: 36px
  fontColor: "#fff", // default: #52525b
  backgroundColor: "#3a4a54", // default: #fafafa
  fontSize: "17px", // default: 12px
}
```

### sort (Client-Side)

La ordenación se realiza **completamente en el navegador** (sin actualización de Ninox, sin campo auxiliar). El estado (columna activa, dirección) se gestiona **internamente** y se persiste, como el scroll/los grupos, en **localStorage**.

**Columna de encabezado** — activa `sort` y establece el tipo:

- `**sort.enabled**`: `true` — el encabezado de columna es clicable y muestra iconos de ordenación.
- `**sort.type**`: `'text'` (por defecto), `'number'` o `'date'`.

**Celda de fila** — valor bruto opcional para la comparación:

- `**sort.value**`: valor por el que se ordena. Si no se establece, se usa el `value` de la celda (no adecuado si `value` contiene widgets/HTML — en ese caso, establece siempre `sort.value`).

**Ciclo de clic:** primer clic → ascendente, segundo → descendente, tercero → ordenación desactivada.

**Iconos de ordenación (chevrons de contorno):** colores opcionales mediante `header.sortIcons` (se establecen como variables CSS en el encabezado):

| Prop | Significado | Por defecto (CSS-fallback) |
|------|-----------|-------------------------|
| `color` | Color de los carets **inactivos** | `rgba(0,0,0,0.35)` |
| `activeColor` | Color del caret **activo** (dirección de ordenación actual) | `#000000` |
| `hoverColor` | Color de ambos carets en **hover** sobre el encabezado ordenable (carets inactivos; el caret activo mantiene `activeColor`) | `rgba(0,0,0,0.55)` |

Con **agrupación** (`groupedBy`), la ordenación se aplica solo **dentro de cada grupo**, no entre grupos.

```javascript
header: {
    showHeader: true,
    sortIcons: {
        color: 'rgba(0,0,0,0.4)',
        activeColor: '#000000',
        hoverColor: 'rgba(0,0,0,0.6)'
    },
    columns: [{
        title: 'Name',
        width: 'fraction',
        sort: { enabled: true, type: 'text' }
    }, {
        title: 'Betrag',
        width: '120px',
        sort: { enabled: true, type: 'number' }
    }]
},
table: myList.[{
    recordId: Nr,
    columns: [{
        field: 'Name',
        value: Name,
        width: 'fraction'
    }, {
        field: 'Betrag',
        value: text(Betrag) + ' €',
        sort: { value: number(Betrag) },
        width: '120px'
    }]
}]
```

### emptyTable

Con el bloque `**emptyTable**` defines **qué se muestra cuando no hay datos disponibles**, es decir, cuando `data: []` está vacío. Puedes adaptar el texto y el diseño del estado vacío, mejorando así notablemente la experiencia de usuario.

- `**title**`: el texto principal que se muestra (normalmente destacado, p. ej. «No se encontraron entradas»).
- `**value**`: texto adicional opcional o elementos HTML dentro del área vacía.
- `**backgroundColor**`: color de fondo del área vacía, p. ej. `"white"` o `"#f4f6ff"`.

**Comportamiento por defecto:**
Sin `emptyTable` se muestra un texto simple «Sin resultados».

💡 **Consejo profesional:**
Los estados vacíos no son errores, son tu escenario. Úsalos para explicaciones, mensajes motivadores o un call-to-action claro («Crear ahora una nueva entrada»). Así tus usuarios no se quedan colgados en el vacío — en el sentido más literal.


```javascript
emptyTable: {
            title: "", // Text der bei einer leeren Tabelle in einem abgerundeten Badge dargestellt wird.
            backgroundColor: "", // Hintergrundfarbe in HEX angeben
            value: "" // Hier kannst du eigene Widgets oder komplexe Ansichten einfügen, die den Title überschreiben.
        },
```

![](https://framerusercontent.com/images/bAxu68gh9ycCRLInmZwR463qpeI.png)

### scrollBar

Con el bloque `**scrollBar**` configuras la **barra de desplazamiento horizontal** de tu tabla. Se muestra cuando tu tabla contiene más columnas de las que caben en el área disponible.

- `**showScrollBar**`: `true` o `false` — determina si la barra de desplazamiento se muestra.
- `**height**`: altura del contenedor de la barra de desplazamiento (p. ej. `"10px"`).
- `**backgroundColor**`: color de fondo del contenedor.
- `**handle.height**`: altura del control deslizante («handle») dentro de la barra.
- `**handle.backgroundColor**`: color del handle de desplazamiento.
- `**handle.borderRadius**`: esquinas redondeadas del control deslizante (p. ej. `"5px"` o `"50%"`).

**Comportamiento por defecto:**
Sin `scrollBar` **no se muestra ninguna barra de desplazamiento visible**, incluso si el contenido desborda horizontalmente.

💡 **Consejo profesional:**
Sobre todo con muchas columnas, una barra de desplazamiento discreta y bien visible ayuda a orientarse. Usa colores suaves y una forma de handle redondeada para que la UI no parezca «técnica» — especialmente en dispositivos táctiles esto vale la pena.


```javascript
scrollBar: { showScrollbar: true, // default: false (Scrollbar ist standardmäßig ausgeblendet)
              height: 40px, // Höhe des Containers. default: 10px
              backgroundColor: "#3a4a54" // default: #f0f0f0
              handle: { height: "20px", // Höhe des Schiebers. default: 90%
                        borderRadius: 20px // default: 20px
                        backgroundColor: "#fff", } // default: #ccc
```

### theme

Con el parámetro `**theme**` puedes activar una **plantilla de diseño predefinida** para tu tabla. El tema influye en el **aspecto visual completo** — desde los colores hasta las tipografías y el layout. Actualmente disponibles:

- `**"clean-white"**`
- `**"naked"**`

**Importante:**
Para que el tema surta efecto correctamente, el `uniqueId` del `theme` debe **usar el mismo **`**uniqueListId**`** que tu tabla**. Solo así el tema sobrescribe de forma específica las reglas de estilo correctas mediante CSS.

**Comportamiento por defecto:**
Si no se establece ningún `theme`, se usa el diseño estándar del widget arcCustomTable (con sombra ligera, alto contraste).


```javascript
theme: arcCustomThemeCleanWhite({
                    uniqueId: "Projektliste Demo 1"
                }),
```

### styles

La misma bolsa `styles` en cada nivel. Preferiblemente un objeto con nombres similares a CSS (`backgroundColor`, `fontColor`, `paddingX`). También funciona un string CSS.

| Nivel | Prop | Afecta a |
|---|---|---|
| Tabla | `styles: { backgroundColor, borders, … }` | Superficie + borde. Por defecto para todas las filas |
| Header | `header: { backgroundColor }` o `header.styles` | Fila de encabezado |
| Celda de header | `header.columns[n].styles` | Una celda de encabezado |
| Fila | `table: [{ styles: { backgroundColor } }]` | Todas las celdas de esta fila |
| Columna / celda | `table: [{ columns: [{ styles: { backgroundColor } }] }]` | Una celda |
| Footer | `footer.styles` | Footer |

La columna sobrescribe a la fila, la fila sobrescribe a la tabla. `styles` prevalece sobre `rowColor` / `backgroundColor` plano.

```javascript
styles: {
        backgroundColor: "#ffffff",
        borderColor: "#e4e4e7",
        borders: { outer: true, columns: false, rows: true }
    },
header: {
        backgroundColor: "#fafafa"
    },
table: myList.[{
        recordId: Nr,
        styles: {
            backgroundColor: "#f4f4f5"
        },
        columns: [{
                value: Name,
                width: "fraction",
                styles: {
                    fontWeight: "600"
                }
            }]
    }]
```

### footer

Con el bloque `**footer**` puedes mostrar un **área inferior** debajo de la tabla, p. ej. para botones o información. Es ideal para **zonas de acción** como «Crear nueva entrada» o para mostrar totales, filtros o información de estado.

- `**showFooter**`: `true` o `false` — activa o desactiva el footer.
- `**showActionButton**`: muestra el botón estándar *«Crear nuevo registro»*.
- `**actionButtonTitle**`: texto individual para el botón de acción.
- `**backgroundColor**`: color de fondo del footer.
- `**leftSideContent**`: contenido libre (p. ej. texto, HTML, iconos) para el lado izquierdo.
- `**rightSideContent**`: contenido para el lado derecho, p. ej. indicadores de estado, totales o información.

**Comportamiento por defecto:**
Sin el bloque `footer` no se muestra ningún footer. Si se establece `showFooter: true` sin más opciones, aparece un área de footer vacía.


```javascript
footer: {
            showFooter: true,
            showActionButton: true,
            backgroundColor: "",
            actionButtonTitle: "Neue Aufgabe hinzufügen",
            leftSideContent: "",
            rightSideContent: "Gesamt: " + cnt(filteredList) + " Aufgaben"
        }
```

Así se ve el footer definido arriba:

![](https://framerusercontent.com/images/juhFlR4IcYzPbEQW10jXQF8x2Ic.png)

## table

En el **área de detalle **`**table**` defines todas las **columnas (columns)** y su **representación, contenido y comportamiento**. Aquí determinas **qué campos de datos se muestran**, cómo se ven y qué ocurre al hacer clic.

- Cada columna se puede **formatear de forma individual** (p. ej. color, alineación, padding, acciones).
- Los valores establecidos aquí **sobrescriben**, en su caso, los ajustes globales del bloque `settings` (p. ej. `fontColor`, `backgroundColor`, `align`, etc.).
- Aquí también puedes controlar **columnas fijas**, **agrupaciones**, **interacciones** y **edición inline**.

## Parámetros de fila y grupo


```javascript
projectList.[{
                recordId: Nr,
                rowColor: "",
                rowHeight: "auto",
                rowPaddingY: "10px",
                groupRowColor: "",
                groupRowSettings: {
                    height: "",
                },
```

- `**recordId**`:
Debe ser el `**Nr**` del registro (es decir, el ID interno en Ninox).
*Importante para la asignación de acciones, p. ej. abrir, editar, eliminar.*

- `**rowColor**`:
Establece el **color de fondo** de la fila correspondiente.
*Acepta valores HEX como *`*"#f4f6ff"*`* o *`*"transparent"*`*.*
- `**rowHeight**`:
Determina la **altura de la fila**.
*El valor puede ser *`*"auto"*`* o una medida fija en píxeles como *`*"60px"*`*.*
- `**rowPaddingY**`
→ **Relleno interno** vertical (top/bottom) dentro de las celdas de esta fila.
Se aplica a todas las celdas, **siempre que no tengan un padding individual establecido**.
- `**groupRowColor**`:
Establece el color de fondo para una **fila de grupo** (cuando `groupedBy` está activo).
*Puedes usar valores HEX o acceder dinámicamente a campos (p. ej. *`*row.farbe*`*).*
- `**groupRowSettings.height**`
→ Fija la **altura de la fila de grupo**.
Valor p. ej. `"40px"` o `"auto"`. Solo actúa si la fila es una **fila de grupo**.
- `**styles**`: string CSS para estilo libre — se aplica a cada celda de la fila. Sustituto a largo plazo de `rowColor` y `rowHeight`.

💡 **Consejo profesional:**
Puedes usar **colores calculados dinámicamente** para hacer visible de inmediato la retroalimentación de estado, p. ej. verde para «todo completado», gris para «en curso»:


```javascript
groupRowColor: if cnt(Firma.Projekte) = cnt(Firma.Projekte[Abgeschlossen != null]) then
                    "#F0FFF1"
                else
                    "#eee"
                end,

```

## Filas anidadas (`items[]`)

Las tablas jerárquicas usan la **misma forma de fila** que las `table[]` planas, más hijos anidados opcionales en cada fila.

### Entrada de nivel superior (una u otra)

| Forma | Caso de uso |
|------|----------|
| **`table: [...]`** | Clásico — proyectos existentes, plano + anidado |
| **`items: [...]`** | Nuevo — mismos objetos de fila, raíz estilo Timeline |

**No** establezcas `table` y `items` en la raíz al mismo tiempo. Si ambos están presentes, `table` gana (aviso de desarrollo).

Cada fila puede contener **`items: [...]`** para hijos (profundidad ilimitada). Contraer/expandir es **del lado del cliente** (persistido en `localStorage` por `uniqueListId`).

### Ejemplo — `table[]` con hijos anidados

```javascript
arcCustomTable({
  uniqueListId: "tasks-" + Nr,
  header: {
    columns: [
      { title: " ", width: "40px" },
      { title: "Aufgabe", width: "fraction" },
      { title: "Status", width: "120px" }
    ]
  },
  collapsible: {
    enabled: true,
    expandColumn: 0,
    indentColumn: 1,
    indentSize: "16px"
  },
  table: [{
    id: "p1",
    recordId: Nr,
    columns: [
      { value: "" },
      { value: Bezeichnung },
      { value: Status }
    ],
    items: Unteraufgaben.[{
      id: format(number(Nr), "0000"),
      recordId: Nr,
      columns: [ /* same column layout */ ],
      items: /* deeper levels */
    }]
  }]
})
```

### Ejemplo — `items[]` en la raíz

Mismos objetos de fila; sustituye `table:` por `items:` en la raíz.

### `collapsible`

| Propiedad | Significado |
|----------|---------|
| `enabled` | `true`: las filas padre con hijos muestran un control de expansión |
| `defaultCollapsed` | `true`: todas las filas padre empiezan contraídas |
| `expandColumn` | Índice de columna para el control (por defecto `0`) |
| `indentColumn` | Índice de columna para el padding de profundidad (por defecto `1`) |
| `indentSize` | Padding por nivel, p. ej. `"16px"` |
| `iconSize` | Tamaño de caret por defecto sin iconos propios — p. ej. `"16px"` (por defecto) |
| `iconExpand` / `iconCollapse` | Descriptores de widget anidados opcionales (p. ej. `arcCustomIcon`) — sustituye al caret por defecto |

**No se puede combinar con `groupedBy`** en la misma configuración de tabla.

La ordenación del lado del cliente se desactiva cuando hay filas anidadas — el orden viene de Ninox (estructura `table` / `items`).

### `span` / `colspan` en celdas

Todas las filas comparten el **mismo número de columnas de encabezado**. Para mostrar una fila padre con una celda ancha que abarque varias columnas, usa **`span`** (alias **`colspan`**) en la celda:

```javascript
// Header: 4 columns (Expand | Name | Status | Date)
columns: [
  { value: "" },
  { value: "Team Nord — 3 members", span: 3 }  // 1 + 3 = 4
]
// Child row: four normal cells (span defaults to 1)
columns: [
  { value: "" },
  { value: "Anna M." },
  { value: badgeWidget },
  { value: "14.06.2026" }
]
```

**No válido:** una fila padre con 2 entradas en `columns` y una fila hija con 4 entradas **sin** `span` — las columnas quedarán desalineadas.

Los valores opcionales **`width`** / **`minWidth`** en una celda siguen sobrescribiendo el ancho mínimo de esa celda; las pistas de la cuadrícula provienen de `header.columns`.

## columns

En el array `columns` defines las **columnas individuales de tu tabla** — es decir, cómo se ven, cómo se llaman y qué se muestra en ellas. Cada columna se describe mediante su propio objeto dentro de `columns`.

**Aspectos básicos que puedes configurar:**

- `**field**`: el campo de datos que se muestra — debe coincidir con los valores en `data.table.columns`.
- `**title**`: el nombre visible de la columna en el encabezado.
- `**width**`: el ancho de la columna — fijo, p. ej. `"200px"` / `"10%"`. Para columna flexible: `"fraction"`, `"auto"` o `""` (vacío) — misma lógica de grid (`minmax(30px, 1fr)`, también combinada con columnas fijas).
- `**align**`: alineación del contenido — `"left"`, `"center"` o `"right"`.
- `**backgroundColor**`** / **`**color**`: color de fondo y de texto de la columna.
- `**paddingX**`** / **`**paddingY**`: espaciados internos horizontal y vertical.
- `**truncate**`: si el texto debe recortarse y añadirse «…».
- `**fixed**`: `"left"` o `"right"`, para fijar una columna.
- `**styles**`: string CSS para estilo libre de la celda (celda de encabezado o de cuerpo). Sobrescribe todas las demás props de estilo.
- `**actions**`: acciones como `update`, `popup`, `change`, `openFullscreen`, `delete`.


```javascript
columns: [{
                field: "",
                title: "",
                value: "",
                width: "",
                align: "",
                fixed: "",
                paddingX: "",
                paddingY: "",
                groupExpandAction: "",
                groupValue: "",
                actions: [{
                    recordId: "",
                    type: "",
                    field: "",
                    value: ""
                }]
        }
```

### Expandir grupos con groupExpandAction

Con ***groupExpandAction*** puedes determinar en qué columnas debe activarse la función de expandir. Esto solo funciona si has configurado una agrupación en tu tabla. Si la activas en al menos una columna (con groupExpandAction:true), se desactiva automáticamente en las demás columnas, salvo que también la actives ahí en los parámetros.


```javascript
groupExpandAction: true, // Aktiviert das Ausklappen der Tabellenzeilen durch Klick auf die Spalte.
groupExpandAction: false, // Deaktiviert das Ausklappen der Tabellenzeilen durch Klick auf die Spalte.
groupExpandAction: "", // default: true
```

<iframe src="https://www.youtube.com/embed/Ez-IlPbx7kI?iv_load_policy=3&rel=0&modestbranding=1&playsinline=1&autoplay=0&mute=1" data-thumbnail="High Quality" frameborder="0" allow="presentation; fullscreen; accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture"></iframe>### actions

Con el parámetro `**actions**` puedes definir **qué debe ocurrir cuando un usuario hace clic en una celda o la edita**. Las acciones se establecen a **nivel de celda dentro de **`**columns**` (en `data.table`) y hacen que tu tabla sea interactiva.

**💡 Nota:** puedes **combinar varias acciones** insertándolas como array.
Ejemplo:


```javascript
actions: [{
    recordId: Nr,
    type: "popup"
    }, {
    recordId: Nr,
    type: "change",
    field: "A"
}],
```

#### Acción: popup

Con `**type: "popup"**` defines una acción que, al hacer clic en la celda, abre el **registro vinculado como popup** directamente dentro de Ninox. Así los usuarios pueden ver o editar detalles sin abandonar la vista actual.

- `**recordId**` debe ser el `Nr` del registro correspondiente.
- Esta acción se aplica **siempre a toda la celda**, no solo al texto.
- Con `showPopupButton: true` puedes mostrar además un botón que dispara explícitamente el popup — útil si quieres separar visualmente editar y abrir.


```javascript
actions: [{
  recordId: Nr,
  type: "popup", // Text. Muss genau so geschrieben werden.
  showPopupButton: true, // true oder false zum Ein- oder Ausblenden des Open-Buttons
  popupButton: arcCustomButton({
        uniqueId: "ButtonProjekt" + Nr,
        icon: "",
        title: "Open",
        fontSize: "13px",
        fontColor: "",
        iconColor: "",
        backgroundColor: "",
        borderColor: ""
    })
}]
```

En lugar de hacer clicable toda la celda, también puedes **mostrar un botón individual dentro de la celda** — p. ej. con el widget `arcCustomButton`.

Para ello usas:

- `**showPopupButton: true**` — activa el botón.
- `**popupButton**` — contiene el elemento de botón renderizado, p. ej. mediante `arcCustomButton()`.

#### Acción: delete

Con `**type: "delete"**` puedes **eliminar** el registro asociado al hacer clic en una celda. La acción se aplica, como en `popup`, **a toda la celda**.

- `**recordId**` debe apuntar al `Nr` del registro que se va a eliminar.
- No se incorpora ningún diálogo de confirmación adicional — la eliminación se produce directamente.

💡 **Nota:** esta acción debe usarse **solo con precaución** y **claramente visualizada** — p. ej. mediante una columna de icono especial o un botón marcado en rojo dentro de la celda.


```javascript
actions: [{
  recordId: Nr,
  type: "delete" // Text. Muss genau so geschrieben werden.
}]
```

#### Acción: update

Con `**type: "update"**` puedes **cambiar directamente un campo del registro vinculado** al hacer clic en una celda — sin popup, sin modo de edición.

- `**recordId**`: el `Nr` del registro que se va a modificar.
- `**field**`: el campo que se actualiza (como texto).
- `**value**`: el nuevo valor que se va a establecer (texto, número, booleano, etc.).


```javascript
actions: [{
  recordId: Nr,
  type: "update",
  field: "G", // Gibt die genaue Field ID des Feldes an, auf das die Aktion angewendet wird.
  value: if erledigt=true then null else true end, // Gibt den Wert an, der bei dem referenzierten Feld eingesetzt werden soll.
}]
```

💡 **Consejo profesional:**
Si no quieres realizar el **borrado directamente**, sino solo **tras una confirmación de seguridad**, puedes usar un pequeño rodeo mediante un **campo auxiliar + trigger**:

- Crea un **campo Sí/No** llamado, p. ej., `trigger_delete`.
- Añade en la tabla una acción `update` que establezca `trigger_delete` en `true`.
- En el trigger de ese campo usas el siguiente diálogo:


```javascript
if dialog("Eintrag Löschen", "Soll der Eintrag wirklich gelöscht werden?", ["Ja, löschen!", "Abbrechen"]) = "Ja, löschen!" then
    delete this
end;
trigger_delete := false
```

#### Acción: openFullscreen

Con `**type: "openFullscreen"**` abres el registro vinculado directamente en **modo pantalla completa** — es decir, como si se abriera de forma clásica y completa en Ninox.

- `**recordId**`: el `Nr` del registro que se va a abrir.

**Escenario de uso:**
Si tienes registros complejos con muchas pestañas o subtablas, `openFullscreen` es mejor opción que `popup`.


```javascript
actions: [{
  recordId: Nr,
  type: "openFullscreen", // Text. Muss genau so geschrieben werden.
}]
```

#### Acción: openRecord

Con `**type: "**openRecord**"**` abres el formulario del registro indicado junto con la tabla correspondiente.

- `**recordId**`: el `Nr` del registro que se va a abrir.


```javascript
actions: [{
    recordId: Nr,
    type: "openRecord",
    showPopupButton: true
    }]
```

### groupValue

Con `**groupValue**` defines el **contenido de la fila de agrupación** en una columna determinada — es decir, lo que se muestra en la fila que, por ejemplo, agrupa varios registros de un proyecto, cliente o estado.

- Puedes usar `groupValue` **en cualquier columna** — incluso varias veces por grupo si lo deseas.
- Puedes incorporar **texto** simple, HTML o incluso **mini-widgets** (p. ej. botones, layouts o indicadores de estado).
- La fila de agrupación se muestra automáticamente cuando `groupedBy` está establecido en el bloque `settings`.


```javascript
groupValue: Projekte.Bezeichnung // Ninox-Felder & -Schreibweisen möglich
groupValue: "Projekt:" + Projekte.Bezeichnung // Text + Ninox-Feld
groupValue: arcCustomProgressBar({
        uniqueId: "",
        width: "",
        fontSize: "",
        fontColor: "",
        backgroundColor: "",
        progressColor: "",
        valueTotal: "",
        valueProgress: "",
        valueText: ""
    }) // Andere Mini-Widgets
```

![](https://framerusercontent.com/images/yp7bARVTzbVbdb75ZV2DK0mkA.png)

### groupByValue

El parámetro `**groupByValue**` permite agrupar tu tabla según **un valor distinto al **`**value**`** mostrado**. Así puedes controlar la visualización de la celda independientemente de la lógica de agrupación.

- `**value**`: lo que el usuario ve en la celda (p. ej. un icono, una etiqueta, un elemento HTML).
- `**groupByValue**`: la fila se agrupa según este valor.

**Fallback:**
Si `**value**` o `**groupByValue**` está vacío, la fila se asigna automáticamente al grupo `**arc-no-value**`.
Este grupo se representa con normalidad, pero también se puede seleccionar u ocultar de forma específica.


```javascript
groupByValue: Projekte.Nr //
groupByValue: Typ // Ninox-Felder
groupByValue: "" // default: es wird nach value gruppiert
```

## Ejemplos

A continuación encuentras algunos ejemplos prácticos que ilustran los diferentes usos de la Custom Table.

### Custom Table Simple

![](https://framerusercontent.com/images/pvAEOaUFtbYHMPzHd4v82XWP3nk.png)


```javascript
let current := this;
let projectList := (select Projekte);
let data := {
        uniqueListId: "Projektliste Offen",
        tableId: tableId(first(projectList)),
        height: "auto",
        groupedBy: "",
        popupButtonTitle: "Open",
        table: projectList.[{
                recordId: Nr,
                rowColor: "",
                groupRowColor: "#eee",
                columns: [{
                        field: "Feldname für Feld-ID",
                        title: "Titel",
                        value: "Ninox Wert",
                        width: "10%"
                    }, {
                        field: "Feldname für Feld-ID",
                        title: "Titel",
                        value: "Ninox Wert",
                        width: "15%"
                    }, {
                        field: "Feldname für Feld-ID",
                        title: "Titel",
                        value: "Ninox Wert",
                        width: "10%"
                    }, {
                        field: "Firma.Name",
                        title: "Firma",
                        value: Firma.Name,
                        width: "10%"
                    }, {
                        field: "Bezeichnung",
                        title: "Projekt",
                        value: Bezeichnung,
                        width: "15%",
                        actions: [{
                                type: "popup",
                                recordId: Nr
                            }, {
                                type: "change",
                                recordId: Nr,
                                field: "A"
                            }]
                    }, {
                        field: "Umsatz",
                        title: "Umsatz",
                        value: text(Umsatz + 1000),
                        width: "20%"
                    }, {
                        field: "Aufgaben",
                        title: "Status Aufgaben",
                        value: "<h1>",
                        width: "100px"
                    }, {
                        field: "Abgeschlossen",
                        title: "Abgeschlossen am",
                        value: text(Abgeschlossen),
                        width: "100px"
                    }]
            }],
        footer: {
            showFooter: false,
            actionButtonTitle: "",
            rightSideContent: "Gesamt: " + cnt(projectList) + " Projekte"
        }
    };
arcCustomTable(data)
```

### Custom Table Complex

![](https://framerusercontent.com/images/OUceOtboxPQac3AJ3N3DEu9yas.png)


```javascript
let current := this;

arcCustomTable(data);
let list := do as transaction
        select Aufgaben
    end;
let filteredList := list[if current.Suche != null then
                testx(Mitarbeiter.'First Name' + "," + text(Mitarbeiter.'Last Name') + "," +
                text(Aufgabe) +
                "," +
                text(Projekte.Bezeichnung) +
                "," +
                ", "(?:" + current.Suche + ")\.*[^]", "gi")
            else
                true
            end and
        if current.'Erledigte einblenden' = true then
                true
            else
                Erledigt != true
            end];
let data := {
        uniqueListId: "Projektliste A",
        tableId: tableId(first(list)),
        height: "500px",
        theme: "",
        groupedBy: text('Gruppieren nach'),
        embedded: false,
        table: filteredList.[{
                recordId: Nr,
                rowColor: "",
                rowHeight: "60px",
                groupRowColor: let currentRecord := this;
                if cnt(filteredList[Projekte.Bezeichnung = currentRecord.Projekte.Bezeichnung and Erledigt = true]) = cnt(filteredList[Projekte.Bezeichnung = currentRecord.Projekte.Bezeichnung]) then
                    "#dcf2de"
                else
                    "#eee"
                end,
                columns: [{
                        field: "Erledigt",
                        title: arcCheckBox({
                                uniqueId: "checkbox all check",
                                value: Erledigt,
                                embedded: true,
                                clickAction: {
                                    recordId: Nr,
                                    fieldId: "G",
                                    value: if cnt(list[Erledigt = true]) != cnt(list) then
                                        false
                                    else
                                        if cnt(list[Erledigt = null]) = cnt(list) then
                                            null
                                        else
                                            if cnt(list[Erledigt = true]) = cnt(list) then
                                                true
                                            end
                                        end
                                    end
                                }
                            }),
                        value: arcCheckBox({
                                uniqueId: "checkbox single check",
                                value: Erledigt,
                                embedded: true,
                                clickAction: {
                                    recordId: Nr,
                                    fieldId: "G",
                                    value: if Erledigt = true then null else true end
                                }
                            }),
                        width: "100px",
                        align: "center",
                        fixed: "left",
                        groupValue: arcCustomIcon({
                                name: "caret-down"
                            })
                    }, {
                        field: "Bild",
                        title: "Logo",
                        width: "150px",
                        value:",
                        actions: [{
                                recordId: Projekte.Nr,
                                type: "popup"
                            }],
                        groupValue: "",
                    }, {
                        field: "Projekt",
                        title: "Projekt",
                        width: "",
                        value: Projekte.Bezeichnung,
                        actions: [{
                                recordId: Projekte.Nr,
                                type: "popup"
                            }],
                        groupValue: Projekte.Bezeichnung,
                    }, {
                        field: "Aufgaben",
                        title: "Aufgabe",
                        value: Aufgabe,
                        align: "left",
                        width: "200px",
                        actions: [{
                                recordId: Nr,
                                type: "popup"
                            }, {
                                recordId: Nr,
                                type: "change",
                                field: "A"
                            }],
                        groupValue: if current.'Gruppieren nach' = 3 then
                            html(---
**{ Aufgabe }**
                            ---)
                        end
                    }, {
                        field: "Mitarbeiter",
                        title: "Mitarbeiter",
                        value:",
                        actions: [{
                                recordId: Mitarbeiter.Nr,
                                type: "popup"
                            }]
                    }, {
                        field: "delete",
                        title: "",
                        width: "40px",
                        align: "center",
                        value: "",
                        actions: [{
                                recordId: Nr,
                                type: "delete"
                            }]
                    }]
            }],
        footer: {
            showFooter: true,
            showActionButton: true,
            actionButtonTitle: "Neue Aufgabe hinzufügen",
            rightSideContent: "Gesamt: " + cnt(filteredList) + " Aufgaben"
        }
    };
arcCustomTable(data)
```
