Skip to content
Sign in
PlaygroundSystem prompt editorMenu

System prompt editor

A textarea editor for system prompts with `{{variable}}` highlighting, a live token count against a budget, optional line numbers, version history with restore, unsaved changes and Cmd/Ctrl+S to save.

Variables highlighted in place, a token budget, version history and Cmd/Ctrl+S to save.

Installation

$ pnpm dlx shadcn@latest add https://chatcn.dev/r/system-prompt-editor.json

Usage

tsx
import { SystemPromptEditor } from "@/components/ui/chatcn/system-prompt-editor"

<SystemPromptEditor
  value={saved}
  onSave={(next) => savePrompt(next)}
  variables={["customer_name", "plan", "language"]}
  versions={versions}
  tokenBudget={2000}
/>

value is the saved prompt. The editor keeps its own draft, shows Unsaved changes with Discard, and saves with the button or Cmd/Ctrl+S. When value changes from outside, a clean draft follows it and a dirty one is kept.

Pass a real tokenizer as countTokens when you have one, for example js-tiktoken or your provider's count endpoint behind a debounce. The default is four characters per token, shown with ≈.

tsx
import { getEncoding } from "js-tiktoken"

const enc = getEncoding("o200k_base")
<SystemPromptEditor countTokens={(text) => enc.encode(text).length} tokenBudget={4000} />

Examples

Line numbers

Numbers count lines you wrote, not lines on screen: a wrapped line keeps one number and the gutter grows with it.

A long prompt with a gutter that follows wrapped lines, scrolling inside the editor.

Token budget

Near the budget with a custom counter, and over it with the overage.

States

Empty, unknown variable, failing save, read only and disabled.

How the highlighting works

A plain textarea with transparent text sits exactly over a highlighted copy of its contents. Both share one scroll container, font, line height, padding and wrapping, and no highlighted run changes weight or spacing, so every glyph lines up. The caret, selection, undo, find, spellcheck and IME composition all stay the browser's own. There is no contenteditable and no editor dependency.

TextTreatment
{{name}} in variablesBlue tint, braces dimmed
{{name}} not in variablesAmber tint and a wavy underline. The header counts them
<rules>, </context>XML-style tags recede to gray
Everything elsePlain

Props

PropTypeNotes
value, defaultValuestringThe saved prompt
onDraftChange(draft) => voidEvery edit
onSave(content) => void | PromiseShows Save. Throw to show the error
versionsPromptVersion[]Newest first: { id, content, savedAt, author?, label? }
onRestore(version) => voidRestoring loads the version as an unsaved draft
countTokens(text) => numberDefaults to a characters / 4 estimate
tokenBudgetnumberAmber from 80%, red past it with the overage
lineNumbersboolean
variablesstring[]Known names. Unset treats every placeholder as known
title, placeholderReactNode, stringDefaults to "System prompt"
maxHeight, minHeightnumber | stringDefaults to 360 and 160. Longer prompts scroll inside
readOnly, disabledboolean

Keyboard

KeyAction
Cmd+S, Ctrl+SSave, anywhere inside the editor. Ignored while an IME is composing
TabLeaves the editor, so keyboard users are never trapped. Indent with spaces
ArrowDown, ArrowUp, EnterIn History: move and restore

Accessibility

  • The title labels the textarea; the highlighted copy is aria-hidden, so screen readers read the prompt once.
  • The token budget is a meter with the full count as its text. Save state is announced through a polite live region.
  • Unknown variables carry an underline as well as color, and the header says how many there are.
  • History is a menu: arrow keys, typeahead and Escape behave as in every menu in the kit.
  • Prompts in right-to-left languages lay out right to left, with the gutter on the start side.

Motion

  • Typing never animates. The Saved check crossfades in with the 150 ms blur swap and fades back after 2 s.
  • The History menu opens from its trigger in 180 ms and skips the animation when opened with the keyboard.