Labs component
NbSpreadsheet is published from the Labs entry. The component is functional and tested, but its API may still change before it graduates into the core library. Pull it in with app.use(NubiscoUILabs) (see Installation).
NbSpreadsheet is a spreadsheet-style data grid. It draws on the same design language as NbBoard and IBM Carbon's Data Spreadsheet pattern: dense rows, hairline borders, a sticky header, an inset focus ring, and tabular numerics throughout.
The contract is intentionally narrow: the host owns the data and the formulas, the component renders the grid, captures input, and emits changes. There is no built-in formula engine — derived cells come from a computed callback the host supplies, so you can wire it into Vuex, Pinia, a worker thread, or a remote service without forking the component.
<template>
<NbSpreadsheet :columns="columns" :rows="rows" @commit="onCommit" />
</template>
<script setup lang="ts">
import { ref } from 'vue'
import type {
ISpreadsheetColumn,
ISpreadsheetRow,
ISpreadsheetChange,
} from '@nubisco/ui'
const columns: ISpreadsheetColumn[] = [
{ id: 'name', label: 'Name', width: 180 },
{ id: 'qty', label: 'Quantity', type: 'number', align: 'right' },
{ id: 'price', label: 'Unit price', type: 'number', align: 'right' },
]
const rows = ref<ISpreadsheetRow[]>([
{ id: 'r1', values: { name: 'Hex bolt M8', qty: 50, price: 0.42 } },
{ id: 'r2', values: { name: 'Washer M8', qty: 50, price: 0.08 } },
])
function onCommit(change: ISpreadsheetChange) {
const row = rows.value.find((r) => r.id === change.rowId)
if (row) row.values[change.columnId] = change.after
}
</script>Installation
NbSpreadsheet lives in the Labs plugin. Install it alongside the core plugin:
import { createApp } from 'vue'
import NubiscoUI, { NubiscoUILabs } from '@nubisco/ui'
import '@nubisco/ui/dist/ui.css'
const app = createApp(App)
app.use(NubiscoUI)
app.use(NubiscoUILabs)
app.mount('#app')You can also import NbSpreadsheet directly for tree-shaken usage:
import { NbSpreadsheet } from '@nubisco/ui'Columns and rows
A column declares its identity, label, type, alignment, default width and whether the column is pinned, sortable, or read-only. The format and parse hooks let you take over display and commit-time validation.
A row carries its own id and a values map keyed by columnId. Rows can pin themselves to the top or bottom of the viewport, which is useful for totals, headers, and reference rows that need to stay on screen while the user scrolls.
const columns: ISpreadsheetColumn[] = [
{ id: 'day', label: 'Day', pinned: 'left', readOnly: true, align: 'center' },
{
id: 'eur',
label: 'Revenue',
type: 'number',
align: 'right',
format: (v) => `€ ${Number(v ?? 0).toLocaleString('pt-PT')}`,
parse: (input) => Number(input.replace(/[^0-9.,-]/g, '').replace(',', '.')),
},
]Derived cells (totals row)
Mark a row as computed: true and pin it to the bottom; provide a computed prop that returns the derived value. The function receives a get(rowId, columnId) reader so you can sum, multiply, or join other cells without storing the result in values.
const rows: ISpreadsheetRow[] = [
{ id: 'r1', values: { item: 'Sensors', qty: 12, unit: 14 } },
{ id: 'r2', values: { item: 'Actuators', qty: 6, unit: 39 } },
{
id: 'totals',
pinned: 'bottom',
computed: true,
values: { item: 'Total' },
},
]
function computeTotal(
rowId: string,
columnId: string,
get: (r: string, c: string) => unknown,
) {
if (rowId !== 'totals') return undefined
if (columnId === 'qty')
return Number(get('r1', 'qty')) + Number(get('r2', 'qty'))
if (columnId === 'unit') return undefined // not summable
if (columnId === 'amt') {
const a = Number(get('r1', 'qty')) * Number(get('r1', 'unit'))
const b = Number(get('r2', 'qty')) * Number(get('r2', 'unit'))
return a + b
}
}Per-cell attributes
cellAttrs(rowId, columnId, row, column) runs lazily for visible cells. Return a partial ISpreadsheetCellAttrs to decorate the cell: tag it read-only, attach a CSS class for conditional styling, set a tooltip, or override the displayed string. Returning void keeps the defaults.
function cellAttrs(rowId: string, columnId: string) {
// Weekend columns are read-only and tinted.
if (columnId === 'sat' || columnId === 'sun') {
return { readOnly: true, className: 'weekend' }
}
}Pair this with a small block of scoped CSS:
:deep(.nb-spreadsheet__cell.weekend) {
background: var(--nb-c-surface);
}Pinned columns and pinned rows
Set column.pinned to 'left' or 'right' to keep a column visible while the user scrolls horizontally. Set row.pinned to 'top' or 'bottom' to do the same vertically. Multiple columns or rows can be pinned at the same edge — the order they appear in the array is preserved.
Sorting
Set column.sortable = true to allow header-click sorting. The first click sorts ascending, the second descending, the third clears the sort. Only the unpinned data rows are sorted; pinned rows stay in their slot.
Keyboard, mouse, and clipboard
| Action | Keys |
|---|---|
| Move the active cell | ↑ ↓ ← → / Tab / Shift+Tab / Enter |
| Start editing | Any printable key / F2 / double-click |
| Commit and move down | Enter |
| Commit and move right | Tab |
| Cancel editing | Esc |
| Clear the selected range | Delete / Backspace |
| Extend the selection | Hold Shift while moving / Shift-click |
| Copy / cut / paste (TSV) | Cmd/Ctrl+C / Cmd/Ctrl+X / Cmd/Ctrl+V |
| Undo / redo | Cmd/Ctrl+Z / Cmd/Ctrl+Shift+Z |
Clipboard payloads are tab-separated values, so copy-paste round-trips cleanly with Excel, Numbers, and Google Sheets.
Theming
NbSpreadsheet is built entirely against the Surface API and inherits any layer the parent provides. Drop it inside .nb-layer-2 or under a dark theme and it will follow without overrides.
The component publishes per-instance CSS custom properties for the few decisions that callers regularly want to tune:
| Token | Default | Purpose |
|---|---|---|
--nbss-row-height | 32px | Data row height |
--nbss-header-height | 36px | Header row height |
--nbss-cell-padding-x | 0.625rem | Horizontal padding inside cells |
--nbss-focus-ring | var(--nb-c-primary) | Inset outline color |
// Denser variant
.my-compact-sheet {
--nbss-row-height: 24px;
--nbss-header-height: 28px;
--nbss-cell-padding-x: 0.4rem;
}