Meet Frontfriend.Your brand. Your rules. Your agents, connected.Explore what’s new

Suggestion

A row of suggested prompts that can start a conversation with one selection.

Interactive preview

The shared preview uses React Base. The source below uses React Aria. Each example runs with local data.

Installation

Complete project setup with react/aria, then install these components:

pnpm exec frontfriend download button message bubble input-group icon -f react --primitive aria

The row scrolls horizontally when its buttons do not fit. Buttons stay in the tab order, and focus brings each one into view. Connect the selected prompt to your send action.

Usage

Complete example

import { Message, MessageContent } from "@/components/ui/message";
import { Bubble, BubbleContent } from "@/components/ui/bubble";
import { Button } from "@/components/ui/button";
import {
  InputGroup,
  InputGroupTextarea,
  InputGroupAddon,
  InputGroupButton,
} from "@/components/ui/input-group";
import { Icon } from "@/components/ui/icon";
import * as React from "react";
export default function AiSuggestionExample() {
  const [prompt, setPrompt] = React.useState("");
  const [text, setText] = React.useState("");
  const [status, setStatus] = React.useState("");
  const fileInput = React.useRef<HTMLInputElement>(null);
  const suggestions = [
    "Add a dark mode toggle",
    "Check the form",
    "Explain this change",
    "Write a test",
  ];
  function send(value: string) {
    if (false || !value.trim()) return;
    setPrompt(value.trim());
    setText("");
    setStatus("Reply added.");
  }
  return (
    <div
      style={{
        width: "100%",
        maxWidth: 560,
        minWidth: 0,
        marginInline: "auto",
        display: "flex",
        flexDirection: "column",
        gap: 24,
        paddingBlock: 16,
        fontSize: "var(--text-sm)",
        lineHeight: 1.5,
      }}
    >
      {prompt && (
        <div style={{ display: "grid", gap: 16 }}>
          <Message align="end">
            <MessageContent>
              <Bubble align="end" variant="muted">
                <BubbleContent>{prompt}</BubbleContent>
              </Bubble>
            </MessageContent>
          </Message>
          <Message>
            <MessageContent>
              <p>
                I will help you with “{prompt}”. Start with a small change, then
                check the result.
              </p>
            </MessageContent>
          </Message>
        </div>
      )}
      <div style={{ display: "grid", gap: 8 }}>
        <div
          role="group"
          aria-label="Suggested prompts"
          style={{
            display: "flex",
            gap: 8,
            overflowX: "auto",
            paddingBlock: 4,
          }}
        >
          {suggestions.map((suggestion) => (
            <span key={suggestion} style={{ flexShrink: 0 }}>
              <Button
                variant="outline"
                size="sm"
                disabled={false}
                onPress={() => send(suggestion)}
              >
                {suggestion}
              </Button>
            </span>
          ))}
        </div>
        <form
          onSubmit={(event) => {
            event.preventDefault();
            send(text);
          }}
        >
          <input
            ref={fileInput}
            type="file"
            multiple
            hidden
            aria-label="Attach files"
            onChange={(event) => {
              const count = event.target.files?.length ?? 0;
              setStatus(`${count} ${count === 1 ? "file" : "files"} attached.`);
              event.target.value = "";
            }}
          />
          <InputGroup>
            <InputGroupTextarea
              aria-label="Prompt"
              placeholder="Ask the agent to change your code..."
              value={text}
              disabled={false}
              onChange={(event) => setText(event.target.value)}
              onKeyDown={(event) => {
                if (
                  event.key === "Enter" &&
                  !event.shiftKey &&
                  !event.nativeEvent.isComposing
                ) {
                  event.preventDefault();
                  send(text);
                }
              }}
            />
            <InputGroupAddon align="block-end">
              <InputGroupButton
                type="button"
                size="icon-sm"
                variant="outline"
                aria-label="Attach files"
                disabled={false}
                onPress={() => fileInput.current?.click()}
              >
                <Icon name="Plus" />
              </InputGroupButton>
              <span style={{ flex: 1 }} />
              <InputGroupButton
                type="submit"
                size="icon-sm"
                variant={
                  text.trim() && !false
                    ? "default"
                    : "secondary"
                }
                aria-label="Send"
                aria-disabled={false || !text.trim()}
              >
                <Icon name="ArrowUp" />
              </InputGroupButton>
            </InputGroupAddon>
          </InputGroup>
        </form>
      </div>
      <p
        role="status"
        style={{
          position: "absolute",
          width: 1,
          height: 1,
          padding: 0,
          margin: -1,
          overflow: "hidden",
          clipPath: "inset(50%)",
          whiteSpace: "nowrap",
          border: 0,
        }}
      >
        {status}
      </p>
    </div>
  );
}

Components and API

This composition has no separate component API. Use the APIs for its installed components.

ComponentTarget reference
button React Aria API
message React Aria API
bubble React Aria API
input-group React Aria API
icon React Aria API