# Prompt Library

> Saved prompts with variables that drop straight into the composer. Handles fill-in forms, slash commands, version history and shared, view-only prompts.

Canonical: https://hextaui.com/blocks/prompt-library
Last updated: 2026-10-29

## Overview

Everyone on a team ends up with the same few prompts in a notes app: write release notes, summarize a PR, draft a support reply. Each time they paste it, they hunt for the parts to change. Prompt Library keeps those prompts next to the chat, marks the parts that change as {{variables}}, and fills them in with a short form before the prompt goes into the composer.

Each variable gets its own color, by its order in the prompt, and keeps it everywhere: the highlight in the editor, the chip in the list, the dot beside its field and the filled-in text in the preview. You can see at a glance which answer goes where. The list stays quiet: each row shows the title, the prompt itself with its variables as chips, who shared it and when it was last used.

The editor highlights variables as you type and suggests them after {{. A name with spaces or symbols gets an inline error with a suggestion, like {{customer_name}}. Each variable is one line that opens to set its name, type (text, long text, options or number), default, hint and whether it can be left empty, and renaming rewrites every place it appears. A live preview fills in sample values, and a prompt can switch the model or override the system prompt.

Search, then press Enter to use the top match. Use opens a form with only the variables the prompt uses, starting at the first one without a default. Enter moves to the next field and submits from the last, and the preview fills in, in each variable’s color, as you type. Type / in Prompt Input to get the same flow from the composer, through Prompt Input’s own menu.

Saving keeps the old text as a version, and History in the editor shows what changed since each one, line by line, with Restore. Duplicate, move, favorite and delete all update at once, and delete offers Undo for five seconds. Import and export use plain JSON. Shared prompts carry their author, and the ones you can’t edit open view only.

## Installation

### Step 1: Get HextaUI Pro

One payment unlocks every block, including future ones. [See pricing](https://hextaui.com/pricing).

### Step 2: Add the Pro registry to components.json

```json title="components.json"
{
  "registries": {
    "@hextaui-pro": {
      "url": "https://hextaui.com/r/pro/{name}.json",
      "headers": {
        "Authorization": "Bearer ${HEXTAUI_PRO_TOKEN}"
      }
    }
  }
}
```

### Step 3: Add your token

Create a token on your [account page](https://hextaui.com/account) and put it in `.env.local` as `HEXTAUI_PRO_TOKEN`.

### Step 4: Add the block

```bash
npx shadcn@latest add @hextaui-pro/prompt-library
```

## Usage

### Store prompts in your database

Pass the prompts and folders you load, and return the saved prompt from onSave. Favorites, moves and deletes update at once and roll back with a message if your API throws. A failed save keeps the editor open with the draft as typed.

```tsx
"use client"

import * as React from "react"

import {
  PromptLibrary,
  PromptLibrarySkeleton,
  type Prompt,
  type PromptDraft,
  type PromptFolder,
} from "@/components/blocks/prompt-library/prompt-library"

async function request<T>(input: string, init?: RequestInit) {
  const response = await fetch(input, {
    ...init,
    headers: { "content-type": "application/json", ...init?.headers },
  })
  if (!response.ok) {
    const body = (await response.json().catch(() => ({}))) as { error?: string }
    throw new Error(body.error ?? "Something went wrong.")
  }
  return (response.status === 204 ? null : await response.json()) as T
}

export function PromptsPage({ onInsert }: { onInsert: (text: string) => void }) {
  const [prompts, setPrompts] = React.useState<Prompt[] | null>(null)
  const [folders, setFolders] = React.useState<PromptFolder[]>([])

  React.useEffect(() => {
    request<{ prompts: Prompt[]; folders: PromptFolder[] }>("/api/prompts").then((data) => {
      setPrompts(data.prompts)
      setFolders(data.folders)
    })
  }, [])

  const replace = (saved: Prompt) =>
    setPrompts((current) => {
      const list = current ?? []
      return list.some((prompt) => prompt.id === saved.id)
        ? list.map((prompt) => (prompt.id === saved.id ? saved : prompt))
        : [saved, ...list]
    })

  const save = async (draft: PromptDraft) => {
    const saved = await request<Prompt>(draft.id ? `/api/prompts/${draft.id}` : "/api/prompts", {
      method: draft.id ? "PUT" : "POST",
      body: JSON.stringify(draft),
    })
    replace(saved)
    return saved
  }

  const patch = async (prompt: Prompt, body: Partial<Prompt>) =>
    replace(await request<Prompt>(`/api/prompts/${prompt.id}`, { method: "PATCH", body: JSON.stringify(body) }))

  if (!prompts) return <PromptLibrarySkeleton />

  return (
    <PromptLibrary
      className="h-svh"
      prompts={prompts}
      folders={folders}
      onSave={save}
      onDelete={async (prompt) => {
        await request(`/api/prompts/${prompt.id}`, { method: "DELETE" })
        setPrompts((current) => current?.filter((item) => item.id !== prompt.id) ?? null)
      }}
      onMove={(prompt, folder) => patch(prompt, { folder })}
      onFavorite={(prompt, favorite) => patch(prompt, { favorite })}
      onUse={(prompt, { text }) => {
        onInsert(text)
        void patch(prompt, { lastUsedAt: new Date().toISOString() })
      }}
    />
  )
}
```

### Type / in Prompt Input

Put PromptLibraryMenu inside PromptInput and saved prompts join your slash commands in Prompt Input’s own menu. A prompt with variables asks for them first, then lands at the caret, ready to send. Use onUse to switch to the prompt’s model.

```tsx
"use client"

import { IconTrash } from "@tabler/icons-react"

import {
  PromptInput,
  PromptInputActions,
  PromptInputBody,
  PromptInputSubmit,
  PromptInputTextarea,
  type PromptMessage,
} from "../prompt-input/prompt-input"
import type { PromptMenuItem } from "../prompt-input/prompt-menu"
import type { Prompt } from "@/components/blocks/prompt-library/prompt-library"
import { PromptLibraryMenu } from "@/components/blocks/prompt-library/prompt-library-menu"

const commands: PromptMenuItem[] = [
  { value: "clear", label: "clear", description: "Start a new chat", icon: <IconTrash />, insert: "" },
]

export function Composer({
  prompts,
  onSend,
  onClear,
  onModelChange,
}: {
  prompts: Prompt[]
  onSend: (message: PromptMessage) => void
  onClear: () => void
  onModelChange: (model: string) => void
}) {
  return (
    <PromptInput onSubmit={onSend}>
      <PromptInputBody>
        <PromptInputTextarea placeholder="Ask anything. Type / for saved prompts" />
        <PromptInputActions>
          <PromptInputSubmit />
        </PromptInputActions>
      </PromptInputBody>
      <PromptLibraryMenu
        prompts={prompts}
        items={commands}
        onSelect={(item) => {
          if (item.value === "clear") onClear()
        }}
        onUse={(prompt) => {
          if (prompt.model) onModelChange(prompt.model)
        }}
      />
    </PromptInput>
  )
}
```

### A team library with permissions

Mark team prompts shared and set canEdit per prompt. Prompts you can’t edit open view only, with Duplicate to edit instead of Save, and readOnly turns off every change for viewers.

```tsx
"use client"

import type { Prompt, PromptDraft, PromptFolder } from "@/components/blocks/prompt-library/prompt-library"
import { PromptLibrary } from "@/components/blocks/prompt-library/prompt-library"

type Role = "owner" | "editor" | "viewer"

type TeamPrompt = Prompt & { ownerId: string; teamId: string | null }

function withPermissions(prompts: TeamPrompt[], userId: string, role: Role): Prompt[] {
  return prompts.map((prompt) => ({
    ...prompt,
    shared: prompt.teamId !== null,
    canEdit: prompt.ownerId === userId || (prompt.teamId !== null && role !== "viewer"),
  }))
}

export function TeamPrompts({
  prompts,
  folders,
  userId,
  role,
  onSave,
  onDelete,
  onUse,
}: {
  prompts: TeamPrompt[]
  folders: PromptFolder[]
  userId: string
  role: Role
  onSave: (draft: PromptDraft) => Promise<Prompt>
  onDelete: (prompt: Prompt) => Promise<void>
  onUse: (text: string) => void
}) {
  return (
    <PromptLibrary
      prompts={withPermissions(prompts, userId, role)}
      folders={folders}
      readOnly={role === "viewer"}
      onSave={onSave}
      onDelete={onDelete}
      onUse={(_, { text }) => onUse(text)}
    />
  )
}
```

## Anatomy

The parts you compose, from the outside in.

| Part | Description |
| --- | --- |
| `PromptLibrary` | Folders and tags in HextaUI’s Sidebar (a sheet on phones), search, the prompt list, the editor, history, the Use form and import and export. |
| `PromptLibraryMenu` | Adds saved prompts to Prompt Input’s / menu and inserts them filled in. |
| `UsePromptDialog` | The fill-in form on its own, for using a prompt from anywhere. |
| `PromptPreview` | A prompt with values filled in, each in its variable’s color. |
| `PromptLibrarySkeleton` | A loading placeholder shaped like the library. |
| `usePromptVariables` | Reads a prompt’s variables, unknown and invalid names, and returns fill(). |

## API reference

### PromptLibrary

Also accepts every div prop.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `prompts` | `Prompt[]` | – | { id, title, body, variables?, tags?, folder?, model?, system?, favorite?, shared?, canEdit?, author?, updatedAt?, lastUsedAt?, versions? }. |
| `folders` | `PromptFolder[]` | – | { id, name, shared? }. Shown in the sidebar and the Move to menu. |
| `onSave` | `(draft: PromptDraft) => Promise<Prompt \| void>` | – | Create when draft.id is missing, update when it’s set. Also used for duplicates, starters and restores. Throw to keep the editor open with your message. |
| `onDelete` | `(prompt) => void \| Promise<void>` | – | Runs after the undo window. Throw and the prompt comes back. |
| `onUse` | `(prompt, { text, values }) => void` | – | Called with the filled-in text. Put it in your composer. |
| `onMove` | `(prompt, folder: string \| null) => void \| Promise<void>` | – | Adds Move to. The card moves at once and goes back if this throws. |
| `onFavorite` | `(prompt, favorite: boolean) => void \| Promise<void>` | – | Adds the star. It fills at once and goes back if this throws. |
| `onImport` | `(drafts: PromptDraft[]) => void \| Promise<void>` | – | Saves an imported file in one call. Without it, each prompt goes through onSave. |
| `readOnly` | `boolean` | `false` | Turns off creating, editing, moving, deleting and importing. Use and export still work. |
| `models` | `{ value, label }[]` | – | Adds a Model picker to the editor. |
| `starters` | `PromptDraft[]` | – | The three prompts offered when the library is empty. |
| `footer` | `ReactNode` | – | Shown under the list, like a composer. Hidden while editing. |
| `undoTimeout` | `number` | `5000` | How long Undo is offered after deleting, in ms. 0 deletes at once. |
| `now` | `Date` | – | Fixes the time used for “used 2h ago”, for tests and screenshots. |

### PromptLibraryMenu

Render inside PromptInput.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `prompts` | `Prompt[]` | – | The prompts to offer after /. |
| `items` | `PromptMenuItem[]` | – | Your own slash commands, listed before the prompts. |
| `onSelect` | `(item) => void` | – | Called when one of your own commands is picked. |
| `onUse` | `(prompt, { text, values }) => void` | – | Called after a prompt is inserted, to record use or switch models. |

### PromptVariable

One entry in a prompt’s variables.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `name` | `string` | – | Letters, numbers and _, not starting with a number. |
| `type` | `"text" \| "longText" \| "select" \| "number"` | `"text"` | The field shown in the Use form. |
| `default` | `string` | – | Used when the field is left empty. Options default to the first. |
| `options` | `string[]` | – | The choices for select. |
| `description` | `string` | – | A hint under the field. |
| `optional` | `boolean` | `false` | Lets the form submit with the field empty. |

### usePromptVariables

Returns { names, variables, unknown, unused, issues, tokens, fill }.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `body` | `string` | – | The prompt text. |
| `definitions` | `PromptVariable[]` | – | Saved definitions. Names in the body without one count as text. |

## Keyboard

| Key | Action |
| --- | --- |
| `Enter` | In search, uses the top match. The match is highlighted in the list. |
| `{` `{` | In the editor, suggests variables. ↑ and ↓ move, Enter or Tab picks, Escape closes. |
| `Enter` | In the Use form, moves to the next field, and inserts from the last one. Shift+Enter adds a line in long text. |
| `⌘` `Enter` | In the Use form, inserts from any field. Ctrl+Enter on Windows and Linux. |
| `⌘` `S` | Saves the prompt in the editor. |
| `/` | In Prompt Input with PromptLibraryMenu, lists saved prompts. |
| `Escape` | Clears the search. In the editor, a variable’s name goes back to what it was. |
| `⌘` `B` | Shows or hides the folders and tags sidebar. Ctrl+B on Windows and Linux. |

## Accessibility

- Each row is a list item whose title is a button that uses the prompt, described by its text, owner and last use. Star and actions sit beside it.
- Variable colors always come with the variable’s name, so nothing depends on color alone, and they turn into underlines in Windows high contrast mode.
- The editor is a combobox while suggesting variables, with the active suggestion announced. Invalid names are listed under the field in a polite live region.
- Each variable row in the editor is a button with aria-expanded that opens its settings. Rows with an error open on their own.
- Saving, using, favoriting, moving, deleting, restoring and importing are announced. Failures show as alerts or toasts with what happened.
- Folders and tags use HextaUI’s Sidebar, with the open filter marked as the current page and counts read out as “3 prompts”. On phones it opens as a sheet. Picking a filter while a draft is unsaved asks before leaving it.
- Text fields are 16px on touch screens so iOS doesn’t zoom, and touch targets grow to 44px. With reduced motion, nothing slides.

## Built with

The free HextaUI components Prompt Library is made from. Each one installs on its own.

- [Alert dialog](https://hextaui.com/docs/alert-dialog) - A confirmation dialog for destructive or important actions that waits for async work, and becomes a bottom sheet on phones.
- [Avatar](https://hextaui.com/docs/avatar) - User photos with an initials fallback, status badges and stacked groups that collapse into a count.
- [Badge](https://hextaui.com/docs/badge) - Status labels with colored dots, removable tags that slide closed and counts that roll to their new value.
- [Button](https://hextaui.com/docs/button) - Buttons in every variant and size, with a built-in loading, success and error flow that skips the spinner for fast requests.
- [Checkbox](https://hextaui.com/docs/checkbox) - A checkbox whose check draws in, with indeterminate parents, groups and labels that share its hover.
- [Collapsible](https://hextaui.com/docs/collapsible) - A panel that shows and hides with height motion you can reverse mid-way, without jumping the layout.
- [Dialog](https://hextaui.com/docs/dialog) - A window over the page for forms and focused tasks, with a pinned header and footer, nesting, and a swipeable bottom sheet on phones.
- [Dropdown menu](https://hextaui.com/docs/dropdown-menu) - A menu of actions and options behind a button, with groups, submenus, checkbox and radio items, and shortcuts.
- [Empty](https://hextaui.com/docs/empty) - A placeholder for screens with nothing to show yet, with an icon, a message and the next action.
- [Field](https://hextaui.com/docs/field) - Labels, descriptions and errors wired to their control, with validation states and layouts for forms.
- [Input](https://hextaui.com/docs/input) - A text input with three sizes, invalid and read-only states, native validation styling and a 16px touch font so phones never zoom in.
- [Input group](https://hextaui.com/docs/input-group) - An input with icons, text, buttons or a keyboard hint attached, sharing one border and focus ring.
- [Kbd](https://hextaui.com/docs/kbd) - Key caps for shortcuts that show the right symbols on every platform, read them out properly and press down with the real keys.
- [Number flow](https://hextaui.com/docs/number-flow) - Animated numbers where only the changed digits spin, with any Intl format and locale.
- [Select](https://hextaui.com/docs/select) - Pick one or more options from a list that opens on the current value, with typeahead, groups and form support.
- [Sheet](https://hextaui.com/docs/sheet) - A panel that slides in from any edge, with swipe-to-dismiss, scroll lock and stacked nesting.
- [Sidebar](https://hextaui.com/docs/sidebar) - An app sidebar that collapses to icons or off canvas, stays pinned under your header, and becomes a swipeable sheet on phones.
- [Skeleton](https://hextaui.com/docs/skeleton) - Placeholders that wait 150ms before showing, take the exact size of the content they wrap, and fade it in without moving anything.
- [Switch](https://hextaui.com/docs/switch) - An on/off toggle you can tap, press or drag, with a thumb that stretches under your finger and an optional pending state for saves.
- [Textarea](https://hextaui.com/docs/textarea) - Multi-line text that grows smoothly with what you write between a minimum and maximum number of rows, with counters, validation and a submit shortcut.
- [Toast](https://hextaui.com/docs/toast) - Brief messages that stack neatly, expand on hover, swipe away, and turn a loading state into success or error in place.

## Code

12 files, added to components/blocks/prompt-library.
