@k2b/ui
Prompts
Alert, confirmation, search, form, and custom dialog flows.
Browser-only alert, ordinary and typed confirmation, search, form, and custom-dialog flows, including a safe nested bare-surface example.
TSX
Copyconst confirmed = await prompts.confirm("Publish this release?", { title: "Publish release", confirmText: "Publish",}); const deleted = await prompts.confirm("Delete Project Atlas?", { title: "Delete project", confirmText: "Delete project", confirmationPhrase: "Project Atlas", variant: "danger",}); const selected = await prompts.search(resolveProjects, { title: "Open project", placeholder: "Search projects...",}); const values = await prompts.form({ title: "New project", confirmText: "Create", fields: { name: { type: "text", label: "Project name", required: true }, },}); await prompts.dialog((close) => <MySurface close={close} />, { surface: "bare", header: false,});prompts opens modal alert, confirmation, input, search, form, and custom-dialog flows. Every method returns a promise, so the calling action can continue from the result.
The application owns the decision, validation rules, and side effect. The prompt owns modal stacking, focus, dismissal, and shared presentation.
Use prompts
Use a prompt when the user must acknowledge information, choose a result, or provide a small amount of data before work continues.
Use a toast for non-blocking feedback. Use PanelDialog for a persistent editor with tabs, sections, or a larger application workflow.
Import
import {
Button,
DialogHeader,
openSpotlightSearch,
prompts,
SpotlightButton,
type PromptSearchItem,
} from "@k2b/ui";Properties
Choose a prompt
| Method | Result | Use |
|---|---|---|
prompts.alert(content, options) |
resolves when closed | Information that requires acknowledgement. |
prompts.success(content, options) |
resolves when closed | Successful result that requires acknowledgement. |
prompts.error(content, options) |
resolves when closed | Failure details that must be read. |
prompts.confirm(content, options) |
boolean | undefined |
One confirm-or-cancel decision. |
prompts.prompt(content, defaultValue, options) |
string | null |
One text value. |
prompts.promptNumber(content, defaultValue, options) |
number | null |
One numeric value with optional min and max. |
prompts.form(config) |
typed values or null |
A small declarative form. |
prompts.search(resolver, options) |
selected item or undefined |
An asynchronous picker. |
prompts.dialog(component, options) |
caller-defined result | Custom content and actions. |
Cancellation returns false, null, or undefined according to the method. Handle it before starting a mutation.
Dialog options
| Property | Purpose |
|---|---|
title, icon |
Name the dialog and add a Tabler icon. |
confirmText, cancelText |
Override action labels. cancelText: false hides the cancel button in prompts.form. |
confirmationPhrase |
Requires an exact typed phrase before prompts.confirm can confirm. |
variant |
Selects primary, success, or danger action treatment. |
size |
Selects small, medium, large, or wide. |
surface |
Uses the standard panel or a caller-owned bare surface. |
header |
Set to false when a custom dialog owns its header. |
cancelBehavior |
prompts.alert can use "ignore" to keep Escape and backdrop clicks from closing it. |
Use surface: "bare" with header: false only when the custom content supplies complete visible panel structure and a close control.
Typed confirmation
Use a non-empty, single-line confirmationPhrase for unusually consequential actions where an ordinary confirm-or-cancel decision does not provide enough friction. Empty, whitespace-only, multiline, and tab-containing phrases are rejected. The confirmation input receives focus, and the primary action remains disabled until the value matches the configured phrase exactly, including capitalization and spaces. Cancellation does not require entering the phrase.
confirmText continues to label the action button; it does not configure the phrase.
const confirmed = await prompts.confirm(
"Delete Project Atlas and all stored data?",
{
title: "Delete project",
confirmText: "Delete project",
confirmationPhrase: "Project Atlas",
variant: "danger",
},
);
if (!confirmed) return;
await deleteProject("atlas");Forms
prompts.form infers its return type from the field schema. Supported field types are:
text, including multiline, password, and markdown options;number;image;pin;select;tags;boolean;datetime, with optional date-only mode;info, which displays content and is excluded from the result.
Fields share label, description, placeholder, required, default, and a validate function where applicable. Form-state validation checks required values, text-length and tag-count constraints, and the custom validator. A required boolean field must be checked. Number bounds and PIN length configure their controls but do not create additional form-state error messages.
const values = await prompts.form({
title: "Add member",
icon: "ti ti-user-plus",
fields: {
name: {
type: "text",
label: "Name",
required: true,
},
role: {
type: "select",
label: "Role",
options: [
{ id: "admin", label: "Admin" },
{ id: "user", label: "User" },
],
},
active: {
type: "boolean",
label: "Active",
default: true,
},
},
});
if (!values) return;Search
The search resolver receives the current query and an AbortSignal. It returns items with a label and optional desc, icon, root-relative previewUrl, or value. Results without desc use a compact single-line row; adding desc expands only that result to the two-line layout. Handle the selected value after the returned promise resolves; result items do not own application side effects.
Search options include placeholder, initialQuery, minQueryLength, debounceMs, emptyText, and noResultsText.
Honor the abort signal when the resolver calls a remote API:
const selected = await prompts.search(
async ({ query, abortSignal }) => {
const response = await fetch(`/api/users?q=${encodeURIComponent(query)}`, {
signal: abortSignal,
});
const users = await response.json();
return users.map((user) => ({
label: user.name,
desc: user.email,
value: user,
icon: "ti ti-user",
}));
},
{
title: "Assign owner",
minQueryLength: 1,
noResultsText: "No matching people.",
},
);Use openSpotlightSearch for application or workspace navigation. It supplies search-oriented defaults around prompts.search. SpotlightButton provides matching triggers for ordinary, compact, chip, sidebar, mobile-sidebar, and icon placements.
The shortcut is one contract shared by every trigger, exported so the
application can bind the same keys it advertises: SPOTLIGHT_SHORTCUT
("mod+shift+k", the binding), SPOTLIGHT_SHORTCUT_LABEL ("⇧⌘K", the chip
text, overridable per button through shortcutLabel), and
SPOTLIGHT_SHORTCUT_TITLE ("Mod+Shift+K", used in the default title).
Custom dialogs
prompts.dialog passes a typed close(result) callback to the component. Use it when the standard methods cannot express the content or actions.
The standard surface can provide the title and close row:
const result = await prompts.dialog<"publish" | "draft">(
(close) => (
<div class="flex justify-end gap-2">
<Button variant="secondary" onClick={() => close("draft")}>
Save draft
</Button>
<Button onClick={() => close("publish")}>
Publish
</Button>
</div>
),
{ title: "Publish changes", icon: "ti ti-rocket" },
);For a bare surface, the caller owns all visible panel structure. DialogHeader remains available for that structure. A prompt opened from inside another prompt is stacked above it; the underlying Solid state stays mounted until the nested prompt closes.
await prompts.dialog<void>(
(close) => (
<section class="my-dialog-surface">
<DialogHeader
title="Review changes"
icon="ti ti-file-check"
close={() => close()}
/>
<p>The caller owns this panel background, spacing, and boundary.</p>
<Button
variant="secondary"
onClick={() => void prompts.confirm("Return to the review?")}
>
Open nested confirmation
</Button>
</section>
),
{ surface: "bare", header: false },
);Accessibility
Use specific titles and action labels. Destructive confirmations should name the object and consequence. Do not hide the cancel action when cancellation is a valid path.
The shared dialog core focuses the first input, textarea, select, or button by default and owns Escape handling. Custom bodies still need semantic headings, labelled controls, native buttons, and an explicit close path.
Search results need unique, descriptive labels. A preview or icon may supplement the label but cannot replace it.
Runtime
Prompts are browser interactions and must be called from hydrated code. Their content is mounted through the shared @k2b/ui portal into one modal dialog stack at runtime.
Do not call a prompt during server rendering. Await it from an event handler, then start the mutation only after a confirmed result.
Example
const removeProject = async () => {
const confirmed = await prompts.confirm(
"Delete Project Atlas and its stored data?",
{
title: "Delete project",
confirmText: "Delete project",
variant: "danger",
},
);
if (!confirmed) return;
await deleteProject("atlas");
};