Installation
Complete project setup with vue/reka, then install these components:
pnpm exec frontfriend download dropdown-menu input-group icon attachment -f vue --primitive reka The selected option updates its trigger label. Use the arrow keys to move through each menu, Enter to select, and Escape to close. Connect the controls to the models and reasoning options supported by your application.
Usage
Complete example
<script setup lang="ts">
import { ref } from "vue";
import { InputGroup } from "@/components/ui/input-group";
import { InputGroupAddon } from "@/components/ui/input-group";
import { Attachment } from "@/components/ui/attachment";
import { AttachmentMedia } from "@/components/ui/attachment";
import { Icon } from "@/components/ui/icon";
import { AttachmentContent } from "@/components/ui/attachment";
import { AttachmentTitle } from "@/components/ui/attachment";
import { AttachmentActions } from "@/components/ui/attachment";
import { AttachmentAction } from "@/components/ui/attachment";
import { InputGroupTextarea } from "@/components/ui/input-group";
import { InputGroupButton } from "@/components/ui/input-group";
import { DropdownMenu } from "@/components/ui/dropdown-menu";
import { DropdownMenuTrigger } from "@/components/ui/dropdown-menu";
import { DropdownMenuContent } from "@/components/ui/dropdown-menu";
import { DropdownMenuRadioGroup } from "@/components/ui/dropdown-menu";
import { DropdownMenuSeparator } from "@/components/ui/dropdown-menu";
import { DropdownMenuGroup } from "@/components/ui/dropdown-menu";
import { DropdownMenuLabel } from "@/components/ui/dropdown-menu";
import { DropdownMenuRadioItem } from "@/components/ui/dropdown-menu";
const args = {
"placeholder": "Describe the change",
};
const focusedMenu = ref("");
const menuFocusStyle = {
outline: "2px solid var(--border-interactive-mid)",
outlineOffset: "2px",
};
function focusMenu(event: FocusEvent, name: string) {
focusedMenu.value =
event.target instanceof HTMLElement &&
event.target.matches(":focus-visible")
? name
: "";
}
const text = ref("");
const files = ref<
Array<{
id: string;
name: string;
file: File;
}>
>([]);
const formRef = ref<HTMLFormElement | null>(null);
const providers = [
{ name: "Provider A", models: ["Fast", "Balanced"] },
{ name: "Provider B", models: ["Precise"] },
];
const fileInput = ref<HTMLInputElement | null>(null);
const model = ref("Balanced");
const effort = ref("Medium");
const status = ref("");
function selectFiles(event: Event) {
const input = event.target as HTMLInputElement;
files.value.push(
...Array.from(input.files ?? []).map((file) => ({
id: crypto.randomUUID(),
name: file.name,
file,
})),
);
input.value = "";
}
function send() {
if (!text.value.trim() && !files.value.length) return;
status.value = `Sent: ${text.value.trim() || "Files only"} (${files.value.length} ${files.value.length === 1 ? "file" : "files"}).`;
text.value = "";
files.value = [];
}
function keydown(event: KeyboardEvent) {
if (event.key === "Enter" && !event.shiftKey && !event.isComposing) {
event.preventDefault();
send();
}
}
function removeFile(id: string) {
files.value = files.value.filter((file) => file.id !== id);
formRef.value?.querySelector("textarea")?.focus();
}
</script>
<template>
<div
style="
width: 100%;
max-width: 560px;
min-width: 0;
margin-inline: auto;
display: flex;
flex-direction: column;
gap: 16px;
padding-block: 16px;
font-size: var(--text-sm);
line-height: 1.5;
"
>
<form ref="formRef" @submit.prevent="send">
<input
ref="fileInput"
type="file"
multiple
hidden
aria-label="Attach files"
@change="selectFiles"
/>
<InputGroup
><InputGroupAddon v-if="files.length" align="block-start"
><div
style="
display: flex;
gap: 8px;
flex-wrap: wrap;
min-width: 0;
width: 100%;
"
>
<Attachment v-for="file in files" :key="file.id" size="sm"
><AttachmentMedia><Icon name="FileText" /></AttachmentMedia
><AttachmentContent
><AttachmentTitle>{{
file.name
}}</AttachmentTitle></AttachmentContent
><AttachmentActions
><AttachmentAction
type="button"
:aria-label="'Remove ' + file.name"
@click="removeFile(file.id)"
><Icon name="X" /></AttachmentAction></AttachmentActions
></Attachment></div></InputGroupAddon
><InputGroupTextarea
v-model="text"
aria-label="Prompt"
:placeholder="args.placeholder"
@keydown="keydown" />
<InputGroupAddon align="block-end"
><div
style="
display: flex;
gap: 8px;
flex-wrap: wrap;
width: 100%;
align-items: center;
"
>
<InputGroupButton
type="button"
variant="outline"
size="icon-sm"
aria-label="Attach files"
@click="fileInput?.click()"
><Icon name="Plus"
/></InputGroupButton>
<div style="margin-inline-start: auto">
<div style="display: flex; flex-wrap: wrap; gap: 8px">
<DropdownMenu
><DropdownMenuTrigger as-child
><InputGroupButton
size="sm"
variant="outline"
:style="
focusedMenu === 'model' ? menuFocusStyle : undefined
"
@focus="focusMenu($event, 'model')"
@blur="focusedMenu = ''"
:aria-label="'Model: ' + model"
>{{ model
}}<Icon
name="ChevronDown" /></InputGroupButton></DropdownMenuTrigger
><DropdownMenuContent
><DropdownMenuRadioGroup v-model="model"
><template
v-for="(provider, index) in providers"
:key="provider.name"
><DropdownMenuSeparator
v-if="index > 0"
/><DropdownMenuGroup
><DropdownMenuLabel>{{
provider.name
}}</DropdownMenuLabel
><DropdownMenuRadioItem
v-for="value in provider.models"
:key="value"
:value="value"
>{{ value }}</DropdownMenuRadioItem
></DropdownMenuGroup
></template
></DropdownMenuRadioGroup
></DropdownMenuContent
></DropdownMenu
>
<DropdownMenu
><DropdownMenuTrigger as-child
><InputGroupButton
size="sm"
variant="outline"
:style="
focusedMenu === 'effort' ? menuFocusStyle : undefined
"
@focus="focusMenu($event, 'effort')"
@blur="focusedMenu = ''"
:aria-label="'Effort: ' + effort"
>{{ effort
}}<Icon
name="ChevronDown" /></InputGroupButton></DropdownMenuTrigger
><DropdownMenuContent
><DropdownMenuGroup
><DropdownMenuLabel>Reasoning effort</DropdownMenuLabel
><DropdownMenuRadioGroup v-model="effort"
><DropdownMenuRadioItem
v-for="value in ['Low', 'Medium', 'High']"
:key="value"
:value="value"
>{{ value }}</DropdownMenuRadioItem
></DropdownMenuRadioGroup
></DropdownMenuGroup
></DropdownMenuContent
></DropdownMenu
>
</div>
</div>
<InputGroupButton
type="submit"
:variant="text.trim() || files.length ? 'default' : 'secondary'"
size="icon-sm"
aria-label="Send"
:aria-disabled="!text.trim() && !files.length"
><Icon name="ArrowUp"
/></InputGroupButton></div></InputGroupAddon
></InputGroup>
</form>
<p
role="status"
style="
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip-path: inset(50%);
white-space: nowrap;
border: 0;
"
>
{{ status }}
</p>
</div>
</template> Components and API
This composition has no separate component API. Use the APIs for its installed components.
| Component | Target reference |
|---|---|
dropdown-menu | Reka UI API |
input-group | Reka UI API |
icon | Reka UI API |
attachment | Reka UI API |