---
title: "Styling"
description: "Style variable references and customize their inline NodeViews."
canonical_url: "https://tiptap.dev/docs/composable-docs/variables/api-reference/rendering"
---

# Styling

Style variable references and customize their inline NodeViews.

Variables are inline, non-editable nodes. Style the default view or extend `Variable` with your own NodeView.

## Default markup and styling

```html
<span data-type="variable" data-variable="greeting" contenteditable="false">Hello, Ada!</span>
```

| Attribute         | Value                                  |
| ----------------- | -------------------------------------- |
| `data-type`       | Always `'variable'`.                   |
| `data-variable`   | The variable name, or `''` when unset. |
| `contenteditable` | Always `'false'`.                      |

There are no default CSS classes or styles. Add outer-element attributes with `HTMLAttributes` (`Record<string, string>`, default `{}`):

```ts
Variable.configure({
  HTMLAttributes: { class: 'my-variable' },
})
```

Style `.my-variable` or `[data-type='variable']` in your stylesheet. Required attributes take precedence. ProseMirror may add `ProseMirror-selectednode` when selected.

## Custom NodeViews

Register your customized extension instead of `Variable`:

```ts
const CustomVariable = Variable.extend({
  addNodeView() {
    return ReactNodeViewRenderer(MyVariableView)
  },
})
```

`ReactNodeViewRenderer` comes from `@tiptap/react`. JavaScript and Vue use their standard [Tiptap NodeView APIs](https://tiptap.dev/docs/editor/extensions/custom-extensions/node-views.md).

- Keep the `variable` node name, `name` attribute, and inline, atomic behavior.
- Use a non-editable `span` wrapper without `contentDOM` or `NodeViewContent`.
- Read the initial value with `getVariable({ editor, name })`. Subscribe to [`variablesUpdate`](https://tiptap.dev/docs/composable-docs/variables/api-reference/extension.md#listen-for-changes) to refresh it; values can change without changing node attributes.
- Your view owns its markup and attributes. Remove listeners and rendered content when it is destroyed.

## Render a value

The default view renders Tiptap JSON with the editor's custom NodeViews, ordinary objects as JSON text, and other values as text. Use the same helper in your own view:

```ts
import { renderVariableValue } from '@tiptap-pro/extension-variable'

const rendered = renderVariableValue({ editor, value })
container.replaceChildren(rendered.dom)
```

Before replacing the result or removing your view, release its nested NodeViews:

```ts
rendered.destroy()
```

This renders a snapshot. Render again when the value changes. Nested NodeViews receive `undefined` from `getPos()` because the value is outside the document. See the [helper's types](https://tiptap.dev/docs/composable-docs/variables/api-reference/extension.md#rendervariablevalue).

## HTML serialization

`addNodeView` controls the editor UI. `renderHTML` exports only an empty reference and any configured `HTMLAttributes`:

```html
<span name="greeting" data-type="variable"></span>
```

The `name` attribute preserves the variable name; `data-type` identifies it to the parser. View-only attributes and values are not exported. Custom `renderHTML` and `parseHTML` implementations should preserve this behavior.

Bound slots use [Slot rendering](https://tiptap.dev/docs/composable-docs/slots/api-reference/rendering.md). Slots controlled by an explicit value or resolver receive `contenteditable="false"`, `aria-readonly="true"`, and `data-variable-readonly="true"` in the editor.

Content Protection replaces concealed references with its own view; always read current values with `getVariable` so concealed values stay hidden.
