Component

Textarea composer

<DomTextareaComposer>

An autosizing multiline field with attachment content, leading controls, trailing actions, and submit behavior inside one visual input.

Playground

Try the composer live

Textarea composer playground

The text area grows until maxRows, while the submit action remains inside the shared input frame.

Playground.vuevue
vue
<script setup>
import { reactive } from 'vue';
import { DomTextareaComposer } from '@getdom/studio/vue';

const data = reactive({
	  "modelValue": "",
	  "id": "",
	  "name": "",
	  "label": "Message",
	  "description": "",
	  "placeholder": "Write a message...",
	  "required": false,
	  "disabled": false,
	  "readOnly": false,
	  "invalid": false,
	  "errors": [],
	  "visible": true,
	  "validators": [],
	  "validateOnBlur": true,
	  "chrome": "field",
	  "rows": 2,
	  "autosize": true,
	  "maxRows": 8,
	  "ariaLabel": "Message",
	  "showSubmit": true,
	  "submitLabel": "Send message",
	  "submitDisabled": false,
	  "submitting": false,
	  "submitOnEnter": true,
	  "allowEmptySubmit": false
	});
</script>

<template>
	<DomTextareaComposer
		v-bind="data"
		@update:modelValue="data.modelValue = $event"
	/>
</template>

Demo

Composer with attachments and controls

Slots accept application-owned attachment previews, tools, model controls, voice actions, and custom submit UI without imposing an upload data model.

northstar-receipt.png
vue
<script setup>
import { ref } from 'vue';
import { DomButton, DomIconButton, DomTextareaComposer } from '@getdom/studio/vue';

const message = ref('Compare this receipt with the reimbursement policy and flag anything that needs review.');
const attachments = ref([
	{
		id: 'receipt',
		name: 'northstar-receipt.png',
		preview: '/assets/blocks/expense-capture/receipt-northstar-coffee.png',
	},
]);
const result = ref('');

/**
 * Removes an application-owned attachment from the composer preview.
 *
 * @param {string} id Attachment identifier.
 * @returns {void}
 */
function removeAttachment(id) {
	attachments.value = attachments.value.filter((attachment) => attachment.id !== id);
}

/**
 * Adds the example attachment to demonstrate an application-owned picker.
 *
 * @returns {void}
 */
function addAttachment() {
	if (attachments.value.some((attachment) => attachment.id === 'receipt')) return;
	attachments.value.push({
		id: 'receipt',
		name: 'northstar-receipt.png',
		preview: '/assets/blocks/expense-capture/receipt-northstar-coffee.png',
	});
}

/**
 * Handles the shared composer's submit event and clears the local draft.
 *
 * @param {string} value Submitted composer text.
 * @returns {void}
 */
function sendMessage(value) {
	result.value = value || `${attachments.value.length} attachment sent`;
	message.value = '';
	attachments.value = [];
}
</script>

<template>
	<div class="w-full max-w-3xl space-y-3">
		<DomTextareaComposer
			v-model="message"
			label="Message"
			placeholder="Ask about the attached context..."
			:rows="2"
			:max-rows="7"
			:allow-empty-submit="attachments.length > 0"
			@submit="sendMessage"
		>
			<template #attachments>
				<article
					v-for="attachment in attachments"
					:key="attachment.id"
					class="group relative flex w-48 items-center gap-2 rounded-lg border border-border bg-secondary/40 p-1.5"
				>
					<img :src="attachment.preview" alt="" class="size-12 rounded-md border border-border object-cover" />
					<span class="min-w-0 flex-1 truncate text-xs font-medium text-canvas-fg">{{ attachment.name }}</span>
					<DomIconButton
						label="Remove attachment"
						size="xs"
						icon="M6 6l12 12M18 6 6 18"
						@click="removeAttachment(attachment.id)"
					/>
				</article>
			</template>

			<template #leading>
				<DomIconButton
					label="Attach context"
					size="sm"
					icon="M12 5v14M5 12h14"
					@click="addAttachment"
				/>
				<DomButton size="sm" variant="ghost">Tools</DomButton>
			</template>

			<template #actions>
				<DomButton size="sm" variant="ghost">Fast model</DomButton>
				<DomIconButton
					label="Start voice input"
					size="sm"
					icon="M12 3a3 3 0 0 0-3 3v6a3 3 0 0 0 6 0V6a3 3 0 0 0-3-3Zm-7 9a7 7 0 0 0 14 0M12 19v3M9 22h6"
				/>
			</template>
		</DomTextareaComposer>

		<p v-if="result" class="text-xs text-muted-fg" role="status">Sent: {{ result }}</p>
	</div>
</template>

Reference

Props

Control props

NameTypeTSDefaultDescription
rowsnumbernumber1Minimum visible text rows.
autosizebooleanbooleantrueGrow and shrink the text area to match its content.
maxRowsnumbernumber8Maximum rows before text scrolls. Use 0 for no maximum.
ariaLabelstringstring'Message'Accessible textarea label used when no visible field label is supplied.

Field props

NameTypeTSDefaultDescription
modelValueanyany''Current field value.
idstringstring''Optional ID override. By default parent forms derive the input ID from the field path using underscores.
namestringstring''Local field name. Parent forms derive the full field path and native HTML name from the form hierarchy.
labelstringstring''Visible field label.
descriptionstringstring''Optional helper copy below the field.
placeholderstringstring''Placeholder shown when the control is empty.
requiredbooleanbooleanfalseMark the field as required.
disabledbooleanbooleanfalseDisable field interaction.
readOnlybooleanbooleanfalseShow the value but prevent editing.
invalidbooleanbooleanfalseMark the field invalid.
errors
ts
[
	{
		name: "Validation name",
		message: "Error message",
	}
]
array | object | stringArray<ErrorsItem
type ErrorsItem = {
	name?: string; // Name
	message?: string; // Message
};
>
[]Validation errors for this field.
visiblebooleanbooleantrueShow or hide the field.
validatorsarrayArray<unknown>[]Validators attached to this field. Use functions in Vue code, or serializable records such as { name: "minLength", props: { min: 2 } } in generated schemas.
validateOnBlurbooleanbooleantrueRun validators when the field loses focus.
chrome'field' | 'none' | falsestring'field'Render default field chrome, or hide chrome while keeping form state wiring.

Submit props

NameTypeTSDefaultDescription
showSubmitbooleanbooleantrueShow the built-in circular submit action.
submitLabelstringstring'Send message'Accessible label for the built-in submit action.
submitDisabledbooleanbooleanfalseDisable submission in addition to field and empty-value checks.
submittingbooleanbooleanfalseShow progress and block repeated submission.
submitOnEnterbooleanbooleantrueSubmit on Enter while Shift + Enter continues to add a new line.
allowEmptySubmitbooleanbooleanfalseAllow attachment-only or control-defined submission when the text is empty.

Auto-generated from Textarea composer.props and inline _edit hints.

Events

NamePayloadDescription
@update:modelValuestringFired when the composer text changes.
@submitstringFired with the current text when the built-in submit action is used.
@keydownKeyboardEventFired before the built-in Enter key behavior runs.
@focusFocusEventFired when the textarea receives focus.
@blurFocusEventFired when the textarea loses focus.

Names auto-detected from defineEmits and source emit() calls; payload and description from __doc.events when present.

Keyboard

  • EnterSubmit when submitOnEnter is enabled.
  • Shift + EnterInsert a new line.

Slots

NameScopeDescription
#attachments{ value, disabled, readOnly }File, image, reference, or context previews shown above the text.
#leading{ value, submit, disabled }Controls at the start of the bottom action row, such as add or tool buttons.
#actions{ value, submit, disabled }Additional controls before the built-in submit action.
#submit{ submit, disabled, submitting, label }Replace the built-in circular submit button.