Skip to content
Star on GitHub
ConversationPrompt starters
Style
Accent
Sound

Built by DesignMe, free to use under the MIT licence.

Expert design and dev support for startups and enterprises.

Book a call

Prompt starters

Suggested tasks that fill the composer in one click. Free and MIT licensed, as HTML or a typed React component, in two styles with light and dark mode.

$npx designme add prompt-starters
Pick a starter or type your own
01

Install

Add it to your project with the CLI.

HTML, CSS and JavaScript

$npx designme add prompt-starters

React

$npx designme add prompt-starters --react

The CLI copies the source into your project, so you own it and can change anything. The HTML version lands in ./designme with the shared core and a demo.html. The React version lands in src/components/designme and needs React 19; it works in Next.js, Vite and Astro.

02

About

What it is for and what it does.

Prompt starters shows a grid of suggested tasks as cards, each with an icon, a short title and a one-line description. Picking a card writes its full prompt into the field below, word by word, and the Send button passes that prompt to your agent.

  • On an empty chat or a new workspace, when people don't know what to ask the agent first.
  • When you want to show what the agent is good at with real, ready-to-send tasks.
  • When you have more suggestions than fit on screen and want a Shuffle button to swap in another set.
03

Usage

Import it and drive it from your own state.

Import

import { PromptStarters } from '@/components/designme/prompt-starters/PromptStarters';

Example

The demo above as a React component, 31 lines you can copy.

'use client';
// The animated preview from designme.dev, rebuilt from the React component: an example of driving it from your own state.
import { useState } from 'react';
import { PromptStarters, type Starter } from './PromptStarters';

/* ---------- Prompt starters: two sets of four; the box starts empty, a pick fills it, Shuffle swaps between them ---------- */
const SETS: Starter[][] = [
  [
    { icon: 'grid', tone: 'blue', title: 'Audit a page', description: 'Check the pricing page for spacing and contrast', prompt: 'Audit pricing for spacing and contrast' },
    { icon: 'edit', tone: 'purple', title: 'Rewrite copy', description: 'Make the onboarding intro shorter', prompt: 'Make the onboarding intro shorter' },
    { icon: 'code', tone: 'green', title: 'Export tokens', description: 'Turn colour and spacing into CSS variables', prompt: 'Export the tokens as CSS variables' },
    { icon: 'layers', tone: 'orange', title: 'Compare frames', description: 'Diff the mobile and desktop frames', prompt: 'Diff the mobile and desktop frames' }
  ],
  [
    { icon: 'chart', tone: 'teal', title: 'Explain a metric', description: 'Why did activation drop last week', prompt: 'Why did activation drop last week?' },
    { icon: 'mail', tone: 'pink', title: 'Draft an email', description: 'Announce the new pricing to customers', prompt: 'Announce the new pricing by email' },
    { icon: 'alert', tone: 'red', title: 'Find a bug', description: 'Trace why checkout logs people out', prompt: 'Fix checkout logging people out' },
    { icon: 'figma', tone: 'purple', title: 'Build from Figma', description: 'Turn the hero frame into code', prompt: 'Build the hero frame in React' }
  ]
];

export default function PromptStartersDemo() {
  const [set, setSet] = useState(0);
  const [picked, setPicked] = useState<string | null>(null);
  return (
    <div style={{ width: '100%' }}>
      <PromptStarters starters={SETS[set]} value={picked} onValueChange={setPicked} onShuffle={() => { setPicked(null); setSet(s => (s + 1) % SETS.length); }} />
    </div>
  );
}
04

Props

Everything PromptStarters takes.

PropTypeWhat it does
startersrequiredStarter[]The starters to show, two to a row. Pass a new set (from onShuffle) and they stagger in again
valuestring | nullThe title of the picked starter. Leave it out to let the cards pick it
defaultValuestring | null
onValueChange(title: string | null) => void
onSend(prompt: string) => voidCalled with the prompt when the person presses Send
onShuffle() => voidAdds a Shuffle button under the field. Swap in another set of starters when it is called
placeholderstring
sentLabelstringShown in the field once the prompt is sent
classNamestring
05

Code

The full source, as the CLI installs it.

PromptStarters.tsxReact · 103 lines
'use client';
import { useEffect, useState } from 'react';
import { cx } from '../lib/cx';
import { useControllable, useReducedMotion } from '../lib/hooks';
import { Icon, type IconName } from '../lib/icons';
import { enter, Tip } from '../lib/motion';
import '../designme.css';
import './prompt-starters.css';

export type StarterTone = 'blue' | 'purple' | 'green' | 'orange' | 'red' | 'yellow' | 'pink' | 'teal';

export interface Starter {
  icon: IconName;
  /** Colours the icon */
  tone?: StarterTone;
  /** A few words, like "Audit a page" */
  title: string;
  /** One quiet line under the title */
  description: string;
  /** The full prompt it writes into the field when picked */
  prompt: string;
}

export interface PromptStartersProps {
  /** The starters to show, two to a row. Pass a new set (from onShuffle) and they stagger in again */
  starters: Starter[];
  /** The title of the picked starter. Leave it out to let the cards pick it */
  value?: string | null;
  defaultValue?: string | null;
  onValueChange?: (title: string | null) => void;
  /** Called with the prompt when the person presses Send */
  onSend?: (prompt: string) => void;
  /** Adds a Shuffle button under the field. Swap in another set of starters when it is called */
  onShuffle?: () => void;
  placeholder?: string;
  /** Shown in the field once the prompt is sent */
  sentLabel?: string;
  className?: string;
}

const TONE: Record<StarterTone, string> = { blue: 'pb', purple: 'pl', green: 'pm', orange: 'pp', red: 'pr', yellow: 'py', pink: 'pk', teal: 'pt' };

/** Suggested first prompts as cards. Picking one writes its prompt into the field, word by word. */
export function PromptStarters({ starters, value, defaultValue = null, onValueChange, onSend, onShuffle, placeholder = 'Pick a starter or type your own', sentLabel = 'Sent to the agent', className }: PromptStartersProps) {
  const [picked, setPicked] = useControllable(value, defaultValue, onValueChange);
  const reduce = useReducedMotion();
  const set = starters.map(s => s.title).join('\u0000');
  // what the field shows: the picked prompt as it is typed, or the line after sending
  const [field, setField] = useState<{ key: number; words: string[]; sent: boolean } | null>(null);
  const [typed, setTyped] = useState(0);

  const fill = (s: Starter) => { setField(f => ({ key: (f?.key ?? 0) + 1, words: s.prompt.split(' '), sent: false })); setTyped(0); };
  const pick = (s: Starter) => { setPicked(s.title); setFilledFor(s.title); fill(s); };
  // a starter picked from outside, through value, fills the field the same way a click does
  const [filledFor, setFilledFor] = useState(picked);
  if (picked !== filledFor) { setFilledFor(picked); const s = starters.find(x => x.title === picked); if (s) fill(s); }
  // a word every 40ms
  useEffect(() => {
    if (!field || field.sent || typed >= field.words.length) return;
    const t = setTimeout(() => setTyped(n => n + 1), typed === 0 ? 0 : reduce ? 0 : 40);
    return () => clearTimeout(t);
  }, [field, typed, reduce]);
  // a new set of starters clears the field
  const [shown, setShown] = useState(set);
  if (shown !== set) { setShown(set); setField(null); setTyped(0); }
  useEffect(() => { if (picked !== null && !starters.some(s => s.title === picked)) setPicked(null); }, [set]); // eslint-disable-line react-hooks/exhaustive-deps

  const send = () => {
    const s = starters.find(x => x.title === picked); if (!s || !field || field.sent) return;
    onSend?.(s.prompt);
    setField(f => ({ key: (f?.key ?? 0) + 1, words: [sentLabel], sent: true }));
    setPicked(null);
  };
  const ready = !!field && !field.sent && picked !== null;

  return (
    <div data-dm="" className={cx('dm-psx', className)}>
      <div className="dm-psx-grid" key={set}>
        {starters.map((s, i) => {
          const e = enter(i * 60), tone = s.tone && TONE[s.tone];
          return (
            <button type="button" key={s.title} className={cx('dm-psx-c', e.className, picked === s.title && 'dm-on')} style={e.style} aria-pressed={picked === s.title} onClick={() => pick(s)}>
              <span className="dm-psx-ic" style={tone ? { color: `var(--dm-${tone})` } : undefined}><Icon name={s.icon} /></span>
              <b>{s.title}</b>
              <span className="dm-d">{s.description}</span>
            </button>
          );
        })}
      </div>
      <div className={cx('dm-psx-in', field && 'dm-has')}>
        <span className="dm-psx-ph">{placeholder}</span>
        <span className="dm-psx-tx" key={field?.key}>
          {field?.sent ? <span className="dm-w">{field.words[0]}</span> : field?.words.slice(0, typed).map((w, i) => <span key={i} className="dm-w">{w} </span>)}
        </span>
        <Tip label="Send">
          <button type="button" className="dm-psx-go" aria-label="Send" disabled={!ready} onClick={send}><Icon name="up" /></button>
        </Tip>
      </div>
      {onShuffle && <button type="button" className="dm-pill dm-ghost dm-sm dm-psx-sh" onClick={onShuffle}><Icon name="refresh" size="xs" />Shuffle</button>}
    </div>
  );
}
prompt-starters.cssReact · 24 lines
/* Generated from designme.dev by scripts/sync.mjs. Edit the site, then sync. */
/* ---------- Prompt starters ---------- */
.dm-psx{display:flex;flex-direction:column;gap:10px;align-items:center}
.dm-psx-grid{display:grid;grid-template-columns:1fr 1fr;gap:var(--dm-s-2);width:100%}
/* white cards with a dimmed grey edge; the picked one's edge is a step stronger */
.dm-psx-c{display:flex;flex-direction:column;gap:6px;align-items:flex-start;text-align:left;padding:var(--dm-s-3);border-radius:14px;background:var(--dm-chip);transition:background .25s,box-shadow .25s}
@media (hover:hover) and (pointer:fine){.dm-psx-c:hover{background:var(--dm-hover)}}
.dm-psx-c.dm-on{background:var(--dm-surface);box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--dm-rail) 50%,transparent),0 1px 2px rgba(0,0,0,.04)}
@media (prefers-color-scheme:dark){:root:not([data-theme="light"]) .dm-psx-c.dm-on{background:var(--dm-hover)}}
:root[data-theme="dark"] .dm-psx-c.dm-on{background:var(--dm-hover)}
.dm-psx-ic{width:28px;height:28px;border-radius:9px;display:grid;place-items:center;background:var(--dm-surface)!important}
.dm-psx-c b{font-weight:500;font-size:13.5px}
.dm-psx-c span.dm-d{font-size:12px;color:var(--dm-muted);line-height:1.4}
.dm-psx-in{display:flex;align-items:center;gap:var(--dm-s-2);width:100%;min-height:46px;padding:6px 6px 6px var(--dm-s-4);border-radius:var(--dm-r-pill);background:var(--dm-surface);box-shadow:var(--dm-shadow);font-size:13.5px;position:relative}
.dm-psx-ph{color:var(--dm-muted);transition:opacity .3s}
.dm-psx-in.dm-has .dm-psx-ph{display:none}
.dm-psx-tx{flex:1;min-width:0}
.dm-psx-tx .dm-w{animation:dm-lvw .4s var(--dm-ease) both}
@keyframes dm-lvw{from{opacity:0;filter:blur(3px)}to{opacity:1;filter:none}}
.dm-psx-go{margin-left:auto;width:32px;height:32px;border-radius:50%;display:grid;place-items:center;background:var(--dm-accent);color:var(--dm-on-accent);flex:none;transition:opacity .3s}
.dm-psx-go[disabled]{opacity:1;cursor:not-allowed;background:var(--dm-dis-hatch),var(--dm-dis-fill);color:var(--dm-dis);box-shadow:var(--dm-soft-ring)}
.dm-psx-sh svg{transition:transform .6s var(--dm-ease)}
@media (hover:hover) and (pointer:fine){.dm-psx-sh:hover svg{transform:rotate(180deg)}}
prompt-starters.htmlHTML · 10 lines
<div class="card bare" data-c="prompt-starters">
  <div class="stage-in">
    <div class="psx">
      <div class="psx-grid"></div>
      <div class="psx-in"><span class="psx-ph">Pick a starter or type your own</span><span class="psx-tx"></span><button class="psx-go" aria-label="Send" data-tip="Send" disabled><svg class="i s"><use href="#i-up"/></svg></button></div>
      <button class="pill ghost sm psx-sh"><svg class="i xs"><use href="#i-refresh"/></svg>Shuffle</button>
    </div>
  </div>
</div>
prompt-starters.cssHTML · 23 lines
/* ---------- Prompt starters ---------- */
.psx{display:flex;flex-direction:column;gap:10px;align-items:center}
.psx-grid{display:grid;grid-template-columns:1fr 1fr;gap:var(--s-2);width:100%}
/* white cards with a dimmed grey edge; the picked one's edge is a step stronger */
.psx-c{display:flex;flex-direction:column;gap:6px;align-items:flex-start;text-align:left;padding:var(--s-3);border-radius:14px;background:var(--chip);transition:background .25s,box-shadow .25s}
@media (hover:hover) and (pointer:fine){.psx-c:hover{background:var(--hover)}}
.psx-c.on{background:var(--surface);box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--rail) 50%,transparent),0 1px 2px rgba(0,0,0,.04)}
@media (prefers-color-scheme:dark){:root:not([data-theme="light"]) .psx-c.on{background:var(--hover)}}
:root[data-theme="dark"] .psx-c.on{background:var(--hover)}
.psx-ic{width:28px;height:28px;border-radius:9px;display:grid;place-items:center;background:var(--surface)!important}
.psx-c b{font-weight:500;font-size:13.5px}
.psx-c span.d{font-size:12px;color:var(--muted);line-height:1.4}
.psx-in{display:flex;align-items:center;gap:var(--s-2);width:100%;min-height:46px;padding:6px 6px 6px var(--s-4);border-radius:var(--r-pill);background:var(--surface);box-shadow:var(--shadow);font-size:13.5px;position:relative}
.psx-ph{color:var(--muted);transition:opacity .3s}
.psx-in.has .psx-ph{display:none}
.psx-tx{flex:1;min-width:0}
.psx-tx .w{animation:lvw .4s var(--ease) both}
@keyframes lvw{from{opacity:0;filter:blur(3px)}to{opacity:1;filter:none}}
.psx-go{margin-left:auto;width:32px;height:32px;border-radius:50%;display:grid;place-items:center;background:var(--accent);color:var(--on-accent);flex:none;transition:opacity .3s}
.psx-go[disabled]{opacity:1;cursor:not-allowed;background:var(--dis-hatch),var(--dis-fill);color:var(--dis);box-shadow:var(--soft-ring)}
.psx-sh svg{transition:transform .6s var(--ease)}
@media (hover:hover) and (pointer:fine){.psx-sh:hover svg{transform:rotate(180deg)}}
prompt-starters.jsHTML · 26 lines
DM.def('prompt-starters', (r, c) => {
  r.innerHTML = r.innerHTML.replace('{up}', I('up')).replace('{shuffle}', I('refresh','xs'));
  const sets = [
    [['grid','var(--pb)','var(--pb-bg)','Audit a page','Check the pricing page for spacing and contrast','Audit pricing for spacing and contrast'],['edit','var(--pl)','var(--pl-bg)','Rewrite copy','Make the onboarding intro shorter','Make the onboarding intro shorter'],['code','var(--pm)','var(--pm-bg)','Export tokens','Turn colour and spacing into CSS variables','Export the tokens as CSS variables'],['layers','var(--pp)','var(--pp-bg)','Compare frames','Diff the mobile and desktop frames','Diff the mobile and desktop frames']],
    [['chart','var(--pt)','var(--pt-bg)','Explain a metric','Why did activation drop last week','Why did activation drop last week?'],['mail','var(--pk)','var(--pk-bg)','Draft an email','Announce the new pricing to customers','Announce the new pricing by email'],['bug','var(--pr)','var(--pr-bg)','Find a bug','Trace why checkout logs people out','Fix checkout logging people out'],['figma','var(--pl)','var(--pl-bg)','Build from Figma','Turn the hero frame into code','Build the hero frame in React']]
  ];
  let si = 0;
  const grid = $('.psx-grid', r), tx = $('.psx-tx', r), inp = $('.psx-in', r), go = $('.psx-go', r);
  function paint(){
    grid.innerHTML = sets[si].map((s, i) => `<button class="psx-c" data-i="${i}"><span class="psx-ic" style="color:${s[1]};background:${s[2]}">${I(s[0] === 'bug' ? 'alert' : s[0])}</span><b>${s[3]}</b><span class="d">${s[4]}</span></button>`).join('');
    stagger($$('.psx-c', grid), 60);
    $$('.psx-c', grid).forEach(b => b.onclick = () => pick(b));
  }
  let k = null;
  function pick(b){
    $$('.psx-c', grid).forEach(x => x.classList.toggle('on', x === b));
    if (k) k.stop(); k = ctl(c); const kk = k; c.offs.push(() => kk.stop());
    tx.innerHTML = ''; inp.classList.add('has'); go.disabled = false;
    kk.run(async () => { for (const w of sets[si][b.dataset.i][5].split(' ')){ tx.insertAdjacentHTML('beforeend', `<span class="w">${w} </span>`); await kk.wait(40); } });
  }
  $('.psx-sh', r).onclick = () => { si = (si + 1) % sets.length; tx.innerHTML = ''; inp.classList.remove('has'); go.disabled = true; paint(); };
  go.onclick = () => { tx.innerHTML = '<span class="w">Sent to the agent</span>'; go.disabled = true; $$('.psx-c', grid).forEach(x => x.classList.remove('on')); };
  // the box starts empty: a starter fills it when someone picks one, in one short line
  paint();
});
06

Questions

What people ask about it.

Can people edit the prompt before sending it?
Not inside this component. Its field displays the picked prompt and is not a text input. To let people edit, look up the starter from the title onValueChange gives you and put its prompt into your own composer.
How do I offer a different set of suggestions?
Pass onShuffle and change starters when it is called. A new set clears the field and the picked starter, then the cards come in again.
Can I pick a starter for the person, for example on first load?
Yes. Set value to the starter's title from your own state and its prompt is typed into the field as if it had been clicked.
07

More in Conversation

Everything for asking, answering and replying, by text or by voice.

Live answerAnswers that stream in with numbered sources and next steps.
ComposerMessage input with attachments, what to work on, a mode and dictation.
Thread panelSwitch between agent threads and reply in place.
Artifact cardA file the agent made, with its versions, Open, Copy and Download.
Rich text editorA minimal editor with an inline AI menu that streams a suggestion to accept or discard.
Tool callEach tool the agent calls, with its input and output.
Voice captureTalk to the agent, then fix what it misheard before sending.
Voice orbA hands-free voice chat, with an orb that shows who is talking.
All 15 in Conversation →