> For the complete documentation index, see [llms.txt](https://docs.rowsncolumns.app/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.rowsncolumns.app/configuration/features/rich-text-formatting.md).

# Rich text formatting

Spreadsheet supports per-segment formatting inside a single cell. A run of bold inside an otherwise plain string, a colored phrase in the middle of a sentence, or `@mention` chips inline with text all round-trip through XLSX and render directly on the canvas.

## Storage model

Per-segment formatting is **not** stored on `CellData`. Cells reference a shared-strings entry via `ss`, and rich entries on the SharedStrings side carry `{ text, runs }`. This mirrors XLSX `<si><r>` semantics — one canonical location per rich string, no per-cell duplication.

```typescript
// libs/spreadsheet-state/types.ts
export type RichSharedString = {
  text: string;
  runs: TextFormatRun[];
};

export type SharedStringValue = string | RichSharedString;
export type SharedStrings = Map<string, SharedStringValue>;
```

Two cells with the same rich text + same runs share a single SharedStrings slot. Two cells with the same text but different runs get separate slots (matches Excel — runs are part of the dedup key).

## TextFormatRun

A run is a `[startIndex, endIndex)` slice over the cell's text plus either a `TextFormat` (text run) or a `Mention` (mention chip).

```typescript
// libs/common-types/index.ts
export type TextFormatRun<
  Format extends TextFormat = TextFormat,
  M extends Mention = Mention,
> =
  | {
      startIndex: number;
      endIndex: number;
      format: Format;
      nodeType: "text";
    }
  | {
      startIndex: number;
      endIndex: number;
      nodeType: "mention";
      mention: M;
    };

export type TextFormat = {
  color?: Color | string;
  fontFamily?: string;
  fontSize?: number;
  bold?: boolean;
  italic?: boolean;
  strikethrough?: boolean;
  underline?: boolean;
  vertAlign?: "superscript" | "subscript";
};
```

A cell with `"Hello world"` where `"world"` is bold red would carry:

```typescript
{
  text: "Hello world",
  runs: [
    { startIndex: 0, endIndex: 6, nodeType: "text", format: {} },
    {
      startIndex: 6,
      endIndex: 11,
      nodeType: "text",
      format: { bold: true, color: "#ff0000" },
    },
  ],
}
```

## Rendering rich text

The canvas grid takes a `getTextFormatRuns` prop. When you don't pass it, cells with rich runs render as plain text — the formatting still exists in the data, but the renderer can't see it.

```tsx
import { CanvasGrid } from "@rowsncolumns/spreadsheet";
import { useSpreadsheetState } from "@rowsncolumns/spreadsheet-state";

function MySpreadsheet() {
  const {
    getCellData,
    getTextFormatRuns, // <- exposed by useSpreadsheetState
    // ...other selectors
  } = useSpreadsheetState({
    sheets,
    sheetData,
    sharedStrings,
    onChangeSharedStrings,
    // ...
  });

  return (
    <CanvasGrid
      sheetId={1}
      getCellData={getCellData}
      getTextFormatRuns={getTextFormatRuns}
      // ...
    />
  );
}
```

`getTextFormatRuns(sheetId, rowIndex, columnIndex)` resolves the cell's `ss` key against the `SharedStrings` map and returns the rich runs (or `undefined` for plain cells).

{% hint style="info" %}
Rich-text rendering needs both `sharedStrings` (provided to `useSpreadsheetState`) **and** `getTextFormatRuns` (forwarded to `CanvasGrid`). The runs live on the SharedStrings side, so without shared strings there's nowhere to read them from. See [Shared strings](/configuration/features/shared-strings.md).
{% endhint %}

## Editing rich text

The default cell editor is a ProseMirror-based rich text editor. Standard shortcuts work out of the box:

| Shortcut               | Effect                      |
| ---------------------- | --------------------------- |
| `Cmd/Ctrl + B`         | Bold the selection          |
| `Cmd/Ctrl + I`         | Italic the selection        |
| `Cmd/Ctrl + U`         | Underline the selection     |
| `Cmd/Ctrl + Shift + X` | Strikethrough the selection |

The toolbar's font color, font size, font family, and decoration buttons also write into the selected range when a cell is open for editing.

When the user commits the edit, the spreadsheet:

1. Builds a `TextFormatRun[]` from the editor state
2. Writes (or reuses) a rich entry in `sharedStrings`
3. Sets `cellData.ss` to point at the slot

`onChange` receives the new value and the runs:

```tsx
onChange(
  sheetId: number,
  cell: CellInterface,
  value: string | boolean,
  textFormatRuns: TextFormatRun[] | null | undefined,
  previousValue?: string | boolean | null,
  isDirty?: boolean,
  previousTextFormatRuns?: TextFormatRun[] | null,
): void
```

`previousTextFormatRuns` lets a host detect a pure-formatting edit (value unchanged, runs differ — for example Cmd+B on existing text) and persist it as a dirty change. `useSpreadsheetState` already handles this.

## XLSX round-trip

Rich text round-trips through XLSX with no extra wiring. On import, `<si><r><rPr/>...<t/></r></si>` blocks parse into `RichSharedString` entries; on export, rich shared strings emit the corresponding `<si><r>` markup.

Supported run properties on the XLSX path:

* `<b/>` — bold
* `<i/>` — italic
* `<u/>` — underline
* `<strike/>` — strikethrough
* `<sz val=N/>` — font size
* `<rFont val="…"/>` — font family
* `<color rgb="…"/>` — ARGB color
* `<color theme="…" tint="…"/>` — theme color

## Caveats

The canvas grid renders runs only when the cell isn't using one of the modes that fundamentally changes how text is painted. Cells in any of these modes fall back to flat-text rendering:

* Chip cells (data validation chips, structured chips)
* `shrinkToFit`
* `textRotation` (any non-horizontal angle, including `"vertical"`)
* `vertAlign` set on the cell-level format (superscript / subscript at the run level is fine)

Mention runs (`nodeType: "mention"`) render as inline chips. See [Mentions](/configuration/features/mentions.md) for the autocomplete plumbing.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.rowsncolumns.app/configuration/features/rich-text-formatting.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
