Skip to content

Textarea

A native textarea with the same field join as Input, and an optional height that grows with the content.

View source View as Markdown

Textarea renders a real <textarea> with the same field join as Input, and can grow its height to fit what is in it. Useful for a message or a note; if the box should keep a fixed height, leave autoSize off.

Grows to six rows, then scrolls.

App.vue
<template>
  <Field.Root
    class="group flex w-[17rem] flex-col gap-1.5"
    :options="{ name: 'message' }"
  >
    <div :class="box">
      <div :class="stack">
        <span :class="line">
          <Textarea
            v-model="message"
            :class="control"
            auto-size
            :max-rows="6"
            rows="2"
            placeholder=" "
          />
        </span>
        <Field.Label :class="label">Message</Field.Label>
      </div>
    </div>

    <Field.Description :class="hint">
      Grows to six rows, then scrolls.
    </Field.Description>
  </Field.Root>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import { Field } from '@maas/mirror/vue/Field'
import { Textarea } from '@maas/mirror/vue/Textarea'
const message = ref('')

const box = [
  'relative isolate flex min-h-24 max-h-48 w-full items-start overflow-y-auto scrollbar-none',
  'rounded-component-lg border-surface border-2 p-[0.875rem]',
  'cursor-text transition-all duration-100 ease-linear [&_*]:transition-all [&_*]:duration-100 [&_*]:ease-linear',
  'focus-within:py-[0.375rem] has-[textarea:not(:placeholder-shown)]:py-[0.375rem]',
  'outline-4 outline-transparent focus-within:focus-ring',
  'group-[[data-invalid=true]:not([data-focused=true])]:border-danger-subtle',
  'group-[[data-invalid=true]:not([data-focused=true])]:bg-danger-subtle',
  'group-data-[disabled=true]:border-disabled-subtle group-data-[disabled=true]:cursor-not-allowed',
].join(' ')

const stack = [
  'group/stack flex max-h-full w-full flex-col-reverse',
  'items-center justify-center gap-0 px-1',
  'focus-within:gap-1 has-[textarea:not(:placeholder-shown)]:gap-1',
].join(' ')

const line = [
  'flex w-full min-h-0 overflow-x-clip',
  'group-focus-within/stack:min-h-[1.4rem]',
  'has-[textarea:not(:placeholder-shown)]:min-h-[1.4rem]',
].join(' ')

const control = [
  'type-component-lg text-surface w-full bg-transparent outline-none',
  'scrollbar-none placeholder:text-transparent',
  '[--mirror-textarea-min-height:2.625rem] [--mirror-textarea-resize:none]',
  'group-[[data-invalid=true]:not([data-focused=true])]:text-danger-on-muted',
  'data-[disabled=true]:text-disabled-solid',
].join(' ')

const label = [
  'type-component-lg leading-[normal]! text-surface-muted',
  'flex h-full w-full items-center [--mirror-label-cursor:text]',
  "before:absolute before:-inset-0.5 before:-z-10 before:content-['']",
  'group-focus-within/stack:h-auto group-focus-within/stack:text-[0.6875rem] group-focus-within/stack:[--mirror-label-cursor:auto]',
  'group-has-[textarea:not(:placeholder-shown)]/stack:h-auto group-has-[textarea:not(:placeholder-shown)]/stack:text-[0.6875rem] group-has-[textarea:not(:placeholder-shown)]/stack:[--mirror-label-cursor:auto]',
  'group-[[data-invalid=true]:not([data-focused=true])]:text-danger-muted',
  'group-data-[disabled=true]:text-disabled-muted',
].join(' ')

const hint = 'type-component-2xs text-surface-muted'
</script>

Usage guidelines

  • maxRows is worked out from the resolved line-height. A line-height of normal is a keyword rather than a length, so the height of one line is measured off the element’s own typography instead, which costs a layout read the first time. An explicit line-height saves it.should
  • Set id on Field.Root rather than on the textarea, because Label points its for at the field’s control ID and an id here leaves it pointing at nothing.must