Textarea

A multi-line text field.

<script setup lang="ts">
  import { ref } from 'vue'
  import { Textarea } from '@hina-ui/vue'

  const text = ref(
    'On my first day at the new school I met a girl on the roof holding an old camera. She said it could photograph tomorrow.',
  )
</script>

<template>
  <Textarea v-model="text" aria-label="Synopsis" autosize class="w-full max-w-md" />
</template>
vue

Usage

import { Textarea } from '@hina-ui/vue'
ts

The field renders a native textarea with v-model bound to its text. It is three rows tall by default and can be stretched vertically from the corner. Attributes it does not declare land on the textarea, so placeholder, maxlength, name and the like work as usual.

<script setup lang="ts">
  import { ref } from 'vue'
  import { Textarea } from '@hina-ui/vue'

  const text = ref('')
</script>

<template>
  <Textarea
    v-model="text"
    aria-label="Comment"
    placeholder="Write a comment"
    class="w-full max-w-md"
  />
</template>
vue

Examples

Auto height

With autosize set the height follows the content and the resize handle is hidden. Pass an object to bound the number of rows; past maxRows the field scrolls inside.

<script setup lang="ts">
  import { ref } from 'vue'
  import { Stack, Textarea } from '@hina-ui/vue'

  const free = ref('')
  const bounded = ref('')
</script>

<template>
  <Stack class="w-full max-w-md">
    <Textarea
      v-model="free"
      aria-label="Unbounded"
      autosize
      placeholder="Grows with the content, no limit"
    />
    <Textarea
      v-model="bounded"
      aria-label="Two to five rows"
      :autosize="{ minRows: 2, maxRows: 5 }"
      placeholder="Starts at two rows, stops at five, then scrolls inside"
    />
  </Stack>
</template>
vue

Sizes

Three sizes, matching the input. With a single line of content the height equals the input of the same size.

<script setup lang="ts">
  import { Stack, Textarea } from '@hina-ui/vue'
</script>

<template>
  <Stack class="w-full max-w-md">
    <Textarea size="sm" :rows="1" aria-label="Small" placeholder="Small" />
    <Textarea size="md" :rows="1" aria-label="Medium" placeholder="Medium" />
    <Textarea size="lg" :rows="1" aria-label="Large" placeholder="Large" />
  </Stack>
</template>
vue

Variants

primary has a border, background and shadow; secondary uses a tinted background; bare is transparent, without a border, shadow, hover fill or container focus ring. Size and padding keep their existing settings.

bare preserves the disabled state and aria-invalid; FormField can display the error message.

<script setup lang="ts">
  import { Textarea, Stack } from '@hina-ui/vue'
</script>

<template>
  <Stack class="w-full max-w-md">
    <Textarea variant="primary" aria-label="primary" placeholder="primary" />
    <Textarea variant="secondary" aria-label="secondary" placeholder="secondary" />
    <Textarea variant="bare" aria-label="bare" placeholder="bare" />
  </Stack>
</template>
vue

States

invalid marks a failed validation and disabled prevents editing.

<script setup lang="ts">
  import { Stack, Textarea } from '@hina-ui/vue'
</script>

<template>
  <Stack class="w-full max-w-md">
    <Textarea
      invalid
      aria-label="Invalid"
      model-value="The synopsis must be at least twenty characters."
    />
    <Textarea
      disabled
      aria-label="Disabled"
      model-value="The synopsis cannot be edited during review."
    />
  </Stack>
</template>
vue

In a form

Inside a FormField the label points at the textarea through for, and the description and error message are rendered by the field and linked to it; validation rules and submission belong to the Form.

20 to 500 characters

<script setup lang="ts">
  import { reactive, ref } from 'vue'
  import * as v from 'valibot'
  import { Button, Form, FormField, Text, Textarea } from '@hina-ui/vue'

  const schema = v.object({
    review: v.pipe(
      v.string('Write a review'),
      v.trim(),
      v.minLength(20, 'At least 20 characters'),
      v.maxLength(500, 'At most 500 characters'),
    ),
  })

  const values = reactive({ review: '' })
  const saved = ref(false)

  async function save() {
    await new Promise(resolve => setTimeout(resolve, 600))
    saved.value = true
  }
</script>

<template>
  <Form v-slot="{ submitting }" :values="values" :rules="schema" class="w-80" @submit="save">
    <FormField name="review" label="Review" description="20 to 500 characters" required>
      <Textarea v-model="values.review" />
    </FormField>
    <Button type="submit" :loading="submitting" class="self-start">Post</Button>
    <Text v-if="saved" tone="muted" size="sm">Review posted.</Text>
  </Form>
</template>
vue

Behaviour

  • Hover, focus, invalid and disabled behave the same as Input.
  • Auto height is recomputed on input and on v-model changes; the lower bound defaults to rows.
  • Content beyond the visible rows scrolls inside the field, with the same scrollbar as ScrollArea.
  • Clicking blank space inside the field also focuses it.

Accessibility

  • Renders a native textarea, so keyboard and screen reader behaviour comes from the browser.
  • Pair it with a label element or an aria-label for its name.
  • invalid also sets aria-invalid.

API

Props

Prop
Type
Default
Description
variant
'primary' | 'secondary' | 'bare'
'primary'
Variant
size
'sm' | 'md' | 'lg'
'md'
Size
rows
number
3
Number of rows
autosize
boolean | { minRows?: number; maxRows?: number }
false
Whether the height follows the content
resize
'none' | 'vertical'
'vertical'
Whether it can be stretched; off with auto height
invalid
boolean
false
Whether validation failed
disabled
boolean
false
Whether the field is disabled
class
string
—
Classes appended to the root element
Event
Payload
Description
update:modelValue
value: string
The text changed

Exposed methods

input exposes the native HTMLTextAreaElement for reading or setting the selection. focus() focuses it without scrolling the page. Available after mounting on the client.