Skip to content
Component installs need the registry setup
VegaStack Design

Template Editor

A plain-paragraph editor for text with placeholders — @ opens a searchable token list, a picked token becomes a chip, and the value stays plain text.

Status
stable
Since
0.23.72
Accessibility pattern
multiline textbox + listbox picker via aria-activedescendant

Last updated

Type @ or {{ to insert a spec. Enter starts a new paragraph.

Recessed {{power}} downlight with {{efficacy}} efficacy and {{cri}} colour rendering.

Fits a {{cutout}} cut-out.

Install

Add Template Editor from the VegaStack registry. The CLI verifies the item's integrity hash before writing it.

pnpm dlx shadcn@latest add @vegastack/template-editor

It also adds the sanctioned engines to your package.json: @tiptap/react, @tiptap/starter-kit, @tiptap/pm, @tiptap/extensions, @tiptap/suggestion.

Usage

import { TemplateEditor } from "@/components/ui/template-editor";

const specs = [
  { id: "power", label: "Power", hint: "W" },
  { id: "efficacy", label: "Efficacy", hint: "lm/W" },
];

<Field>
  <FieldLabel>Marketing description</FieldLabel>
  <TemplateEditor
    value={template}
    onChange={setTemplate}
    tokens={specs}
    placeholder="Type @ to insert a spec"
  />
</Field>;

The value is plain text. Each placeholder is written as {{id}}, paragraphs are separated by a blank line (\n\n) and a single \n is a line break inside a paragraph, so "Recessed {{power}} downlight\n\nFits a {{cutout}} cut-out." is two paragraphs with two chips. The string round-trips exactly: rendering a value and changing nothing never calls onChange.

Scope

  • Plain paragraphs only. No Markdown, bold, lists, links, toolbar or slash menu — reach for TextEdit when the text needs formatting.
  • Placeholders are atomic. A chip shows the token's label, deletes with one Backspace, and is stored by id, so renaming a spec relabels every chip without touching the value.
  • Nothing is lost. An {{id}} whose id is not in tokens still renders, as the raw id in the destructive style; invalidTokenIds marks known tokens the same way.
  • Filling the placeholders is the caller's job — the editor only writes the template.

Examples

Placeholders from @

Type @ (at the start of a word) or {{ (anywhere) to open the list, keep typing to filter it by label, id or hint, then press Enter or Tab. The box below shows the stored string.

Type @ or {{ to insert a spec. Enter starts a new paragraph.

Recessed {{power}} downlight with {{efficacy}} efficacy and {{cri}} colour rendering.

Fits a {{cutout}} cut-out.

Empty

The placeholder shows until the first character, in the muted placeholder colour Textarea uses.

Unknown and invalid placeholders

invalidTokenIds turns a known token's chip destructive (a spec with no value on this product); an id missing from tokens renders as its raw id in the same style.

Colour temperature has no value on this product, and “finish” is not a spec.

Invalid and disabled

Inside an invalid Field the border stays destructive, focused or not. disabled dims the box and makes it read-only, like a disabled Textarea.

API Reference

PropTypeDefaultDescription
onChange*(value: string) => void—Called with the new template string on every edit. Never called for an unchanged value.
tokens*readonly TemplateEditorToken[]—The placeholders @ or {{ offers, in the order the picker lists them.
value*string—The template: plain text with each placeholder written as {{id}}, and paragraphs separated by a blank line (\n\n). A single \n is a line break inside a paragraph.
aria-describedbystring—Id of the element that describes the editable surface.
aria-invalid"false" | "grammar" | "spelling" | "true" | boolean—Marks the field invalid: a destructive border, like Textarea.
aria-labelstring—Accessible name of the editable surface.
aria-labelledbystring—Id of the element that names the editable surface.
classNamestring—Classes for the bordered root.
disabledbooleanfalseRead-only and dimmed, like a disabled Textarea.
idstring—The editable surface's id (an enclosing Field supplies one).
invalidTokenIdsreadonly string[]—Ids whose chips render in the destructive style (a placeholder the caller rejects).
placeholderstring—Shown while the template is empty.
refReact.Ref<HTMLDivElement>—The bordered root element.

Data attributes and CSS variables on TemplateEditor

AttributeValues
data-disabled""
data-invalid""
data-slot"template-editor"

TemplateEditorToken

PropTypeDefaultDescription
id*string—The stored identifier, written into the value as {{id}}.
label*string—What the chip and the picker show.
hintstring—Muted text beside the label in the picker — a unit or a group.

Accessibility

  • The editable surface is a role="textbox" with aria-multiline="true", named by aria-label, aria-labelledby or an enclosing Field's label; a Field's description, error and invalid state reach it too.
  • The picker is a listbox of options. Focus stays in the textbox, which points at the listbox with aria-controls and at the highlighted option with aria-activedescendant while it is open.
  • The focus cue is text entry's: the box's border darkens subtly over 150ms and nothing fills (data-focus-cue="border"); an invalid box keeps its destructive border. Forced-colours mode restores a system outline.
KeyAction
@ or {{Open the placeholder list
↑ ↓Move through the list
Enter / TabInsert the highlighted placeholder (with the list open)
EscClose the list, keeping what was typed
Backspace / DelDelete the chip before / after the caret as one unit
EnterNew paragraph (with the list closed)
Shift EnterLine break inside the paragraph
ContractStates tested
Behaviourdefault, empty, disabled, invalid, menu-open
Accessibilitylabeled, described, disabled, invalid, semantic-html
Visualdefault, empty, invalid, disabled, invalid-token

Do / Don't

Do
Use TemplateEditor for short marketing or description text built from specs, and pass every spec a product can have as tokens.
Don't
Use it for formatted text (TextEdit), text with no placeholders (Textarea), or to store rendered text — store the template and fill it when you show it.

On this page