Skip to content

Textarea ​

Flexible Textarea component with character counter, automatic height adjustment and W3C accessibility standards compliance.

Description ​

SuTextarea is an enhanced text area component that extends the capabilities of a native <textarea> element. It offers advanced features such as character counter, automatic height adjustment, and complete accessibility management.

Usage examples ​

Simple usage (without SuFormField) ​

Basic Textarea

vue
<script setup>
import { ref } from 'vue'

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

<template>
  <SuTextarea 
    placeholder="Enter your text..."
    :rows="4"
    v-model="text"
  />
</template>

With character counter ​

Character counter

vue
<script setup>
import { ref } from 'vue'

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

<template>
  <SuTextarea 
    placeholder="Your comment..."
    :maxLength="200"
    :showCounter="true"
    :rows="3"
    v-model="comment"
  />
</template>

Automatic height adjustment ​

Auto-resize

vue
<script setup>
import { ref } from 'vue'

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

<template>
  <SuTextarea 
    placeholder="Type your message..."
    :autoResize="true"
    :minRows="2"
    :maxRows="8"
    v-model="message"
  />
</template>

Sizes ​

Available sizes

vue
<template>
  <SuTextarea 
    size="sm" 
    placeholder="Small textarea" 
    :rows="2"
  />
  
  <SuTextarea 
    size="md" 
    placeholder="Medium textarea" 
    :rows="3"
  />
  
  <SuTextarea 
    size="lg" 
    placeholder="Large textarea" 
    :rows="4"
  />
</template>

States ​

Visual states

vue
<template>
  <SuTextarea 
    placeholder="Default state"
  />
  
  <SuTextarea 
    state="error"
    value="Text with error"
  />
  
  <SuTextarea 
    state="success"
    value="Validated text"
  />
  
  <SuTextarea 
    state="warning"
    value="Text with warning"
  />
</template>

Usage with SuFormField ​

The SuTextarea component can be used with SuFormField to benefit from a complete form structure with label, message and state management.

Basic usage with SuFormField ​

vue
<template>
  <SuFormField 
    label="Description" 
    message="Describe your project in a few sentences"
  >
    <template #default="slotProps">
      <SuTextarea 
        placeholder="Enter your description..."
        :rows="4"
        v-bind="slotProps"
        v-model="description"
      />
    </template>
  </SuFormField>
</template>

With slot props destructuring ​

vue
<template>
  <SuFormField 
    label="Comment"
    :required="true"
    message="Share your thoughts"
  >
    <template #default="{ fieldId, messageId, state, disabled }">
      <SuTextarea 
        :id="fieldId"
        :aria-describedby="messageId"
        :state="state"
        :disabled="disabled"
        placeholder="Your comment..."
        :rows="3"
        v-model="comment"
      />
    </template>
  </SuFormField>
</template>

Validation with states ​

Textarea with validation

vue
<script setup>
import { ref, computed } from 'vue'

const message = ref('')
const messageError = ref('')

const validateMessage = () => {
  if (!message.value) {
    messageError.value = 'Message is required'
  } else if (message.value.length < 10) {
    messageError.value = 'Message must contain at least 10 characters'
  } else {
    messageError.value = ''
  }
}

const messageState = computed(() => {
  if (!message.value) return 'default'
  return messageError.value ? 'error' : 'success'
})
</script>

<template>
  <SuFormField 
    label="Message"
    :required="true"
    :state="messageState"
    :message="messageError || 'Write your message'"
  >
    <template #default="slotProps">
      <SuTextarea 
        placeholder="Type your message..."
        :rows="3"
        v-bind="slotProps"
        v-model="message"
        @blur="validateMessage"
      />
    </template>
  </SuFormField>
</template>

With character counter and FormField ​

vue
<template>
  <SuFormField 
    label="Tweet"
    message="Share your thoughts"
    :required="true"
  >
    <template #default="slotProps">
      <SuTextarea 
        placeholder="What's happening?"
        :maxLength="280"
        :showCounter="true"
        :autoResize="true"
        :minRows="2"
        :maxRows="6"
        v-bind="slotProps"
        v-model="tweet"
      />
    </template>
  </SuFormField>
</template>

Auto-resize with FormField ​

vue
<template>
  <SuFormField 
    label="Project description"
    message="Height adjusts automatically to content"
  >
    <template #default="slotProps">
      <SuTextarea 
        placeholder="Describe your project..."
        :autoResize="true"
        :minRows="3"
        :maxRows="12"
        v-bind="slotProps"
        v-model="projectDescription"
      />
    </template>
  </SuFormField>
</template>

Custom label and message ​

vue
<script setup>
import { DocumentTextIcon, InformationCircleIcon } from '@heroicons/vue/24/outline'
</script>

<template>
  <SuFormField 
    label="Content" 
    :required="true"
    message="Write your article"
  >
    <template #label="{ label, required, htmlFor }">
      <label 
        :for="htmlFor" 
        class="flex items-center gap-2 font-bold text-gray-900"
      >
        <DocumentTextIcon class="w-4 h-4" />
        {{ label }}
        <span v-if="required" class="text-red-500">*</span>
      </label>
    </template>
    
    <template #default="slotProps">
      <SuTextarea 
        placeholder="Start writing..."
        :autoResize="true"
        :minRows="4"
        :maxRows="12"
        :maxLength="5000"
        :showCounter="true"
        v-bind="slotProps"
        v-model="content"
      />
    </template>
    
    <template #message="{ state }">
      <div class="flex items-center gap-2 text-sm text-gray-600">
        <InformationCircleIcon class="w-4 h-4" />
        <span v-if="state === 'error'">⚠️ Content is too short</span>
        <span v-else>Write your article (minimum 100 characters)</span>
      </div>
    </template>
  </SuFormField>
</template>

Character limit with feedback ​

vue
<script setup>
import { ref, computed } from 'vue'

const feedback = ref('')

const remainingChars = computed(() => 500 - feedback.value.length)
const feedbackState = computed(() => {
  if (!feedback.value) return 'default'
  if (feedback.value.length < 10) return 'error'
  if (feedback.value.length > 500) return 'error'
  return 'success'
})
</script>

<template>
  <SuFormField 
    label="Product feedback"
    :required="true"
    :state="feedbackState"
    :message="remainingChars < 0 ? 'Limit exceeded' : `${remainingChars} characters remaining`"
  >
    <template #default="slotProps">
      <SuTextarea 
        placeholder="Share your experience..."
        :maxLength="500"
        :showCounter="true"
        :autoResize="true"
        :minRows="3"
        :maxRows="8"
        v-bind="slotProps"
        v-model="feedback"
      />
    </template>
  </SuFormField>
</template>

Disabled or readonly field ​

vue
<template>
  <!-- Disabled -->
  <SuFormField 
    label="Disabled textarea"
    :disabled="true"
    message="This field is temporarily unavailable"
  >
    <template #default="slotProps">
      <SuTextarea 
        value="This content is disabled"
        v-bind="slotProps"
      />
    </template>
  </SuFormField>
  
  <!-- Read-only -->
  <SuFormField 
    label="Read-only textarea"
    message="View only"
  >
    <template #default="{ fieldId, messageId }">
      <SuTextarea 
        :id="fieldId"
        :aria-describedby="messageId"
        :readonly="true"
        value="This content cannot be modified"
      />
    </template>
  </SuFormField>
</template>

API ​

Props ​

PropTypeDefaultDescription
modelValuestringundefinedTextarea value (v-model)
size'sm' | 'md' | 'lg''md'Textarea size
state'default' | 'error' | 'success' | 'warning''default'Visual state
disabledbooleanfalseDisable the textarea
readonlybooleanfalseRead-only textarea
requiredbooleanfalseRequired field
placeholderstringundefinedPlaceholder text
rowsnumber3Default number of rows
minRowsnumber2Minimum rows (auto-resize)
maxRowsnumber10Maximum rows (auto-resize)
maxLengthnumberundefinedMaximum character count
showCounterbooleanfalseDisplay character counter
autoResizebooleanfalseAutomatic height adjustment
spellcheckbooleantrueSpell checking
wrap'soft' | 'hard' | 'off''soft'Line wrap mode

Accessibility props (inherited from SuFormField) ​

When used with SuFormField, the component automatically receives:

PropTypeDescription
idstringUnique field ID (fieldId)
aria-describedbystringAssociated message ID (messageId)
statestringValidation state
disabledbooleanDisabled state

Additional accessibility attributes ​

PropTypeDefaultDescription
ariaLabelstringundefinedAccessible label
ariaInvalidbooleanundefinedIndicates if value is invalid
ariaRequiredbooleanundefinedIndicates if field is required
autocompletestringundefinedHTML autocomplete attribute

Events ​

EventTypeDescription
@update:modelValue(value: string) => voidEmitted when value changes (v-model)
@input(event: Event) => voidEmitted on input
@change(event: Event) => voidEmitted on change
@focus(event: FocusEvent) => voidEmitted on focus
@blur(event: FocusEvent) => voidEmitted on blur
@keydown(event: KeyboardEvent) => voidEmitted on key press
@keyup(event: KeyboardEvent) => voidEmitted on key release

Exposed methods ​

MethodTypeDescription
focus()() => voidFocuses the textarea
select()() => voidSelects the text
blur()() => voidRemoves focus
textareaRefRef<HTMLTextAreaElement>Reference to native textarea element

Accessibility ​

The Textarea component follows WCAG 2.1 AA standards and W3C best practices:

✅ Accessibility features ​

  • ARIA attributes: Complete support (aria-label, aria-describedby, aria-invalid, etc.)
  • Accessible counter: Voice announcements for character limits
  • State messages: Via aria-describedby (messageId) with aria-live
  • Color contrast: WCAG AA compliant ratios (4.5:1)
  • Visible focus: Clear focus indicator with outline
  • Minimum sizes: Touch target respected
  • Dark mode: Adapted contrast
  • Reduced motion: Respects prefers-reduced-motion

🎯 Usage best practices ​

vue
<!-- ✅ GOOD: Usage with SuFormField for complete accessibility -->
<SuFormField 
  label="Description"
  :required="true"
  :state="descriptionError ? 'error' : 'default'"
  :message="descriptionError || 'Minimum 50 characters'"
>
  <template #default="slotProps">
    <SuTextarea 
      placeholder="Describe your project..."
      :maxLength="500"
      :showCounter="true"
      :autoResize="true"
      v-bind="slotProps"
      v-model="description"
    />
  </template>
</SuFormField>

<!-- ✅ GOOD: Standalone usage with manual ARIA attributes -->
<label for="comment-textarea">Comment</label>
<SuTextarea 
  id="comment-textarea"
  aria-describedby="comment-help"
  aria-required="true"
  placeholder="Your comment..."
  v-model="comment"
/>
<span id="comment-help">Share your thoughts (minimum 10 characters)</span>

<!-- ❌ BAD: Without label or description -->
<SuTextarea 
  placeholder="Text..."
  v-model="text"
/>

Advanced features ​

🔄 Smart auto-resize ​

The textarea automatically adjusts its height to content:

  • Minimum height: Defined by minRows
  • Maximum height: Defined by maxRows (scrolls beyond it)
  • Native when possible: CSS field-sizing: content, no JavaScript
  • Fallback: JS measurement based on the actual line height for older browsers

📊 Character counter ​

The counter provides visual and vocal feedback:

  • Dynamic display: chars_entered/max_limit
  • Visual states: Normal, near limit, exceeded
  • Voice announcements: Messages for screen readers
  • Adapted colors: Green, orange, red depending on state

Keyboard navigation ​

KeyAction
TabNavigate to/from textarea
Shift + TabReverse navigation
Ctrl/Cmd + ASelect all text
Ctrl/Cmd + ZUndo
Ctrl/Cmd + YRedo

Advanced usage examples ​

Complete contact form ​

vue
<script setup>
import { ref, computed } from 'vue'

const formData = ref({
  subject: '',
  message: ''
})

const errors = ref({})

const validateForm = () => {
  errors.value = {}
  
  if (!formData.value.subject || formData.value.subject.length < 5) {
    errors.value.subject = 'Minimum 5 characters'
  }
  
  if (!formData.value.message || formData.value.message.length < 20) {
    errors.value.message = 'Minimum 20 characters'
  }
  
  return Object.keys(errors.value).length === 0
}
</script>

<template>
  <form @submit.prevent="validateForm">
    <h2>Contact Us</h2>
    
    <SuFormField 
      label="Subject"
      :required="true"
      :state="errors.subject ? 'error' : 'default'"
      :message="errors.subject || 'Subject of your message'"
    >
      <template #default="slotProps">
        <SuInput 
          placeholder="Subject..."
          v-bind="slotProps"
          v-model="formData.subject"
        />
      </template>
    </SuFormField>
    
    <SuFormField 
      label="Message"
      :required="true"
      :state="errors.message ? 'error' : 'default'"
      :message="errors.message || 'Describe your request in detail'"
    >
      <template #default="slotProps">
        <SuTextarea 
          placeholder="Your message..."
          :maxLength="2000"
          :showCounter="true"
          :autoResize="true"
          :minRows="4"
          :maxRows="12"
          v-bind="slotProps"
          v-model="formData.message"
        />
      </template>
    </SuFormField>
    
    <button type="submit">Send</button>
  </form>
</template>

Editor with statistics ​

vue
<script setup>
import { ref, computed } from 'vue'

const content = ref('')

const stats = computed(() => ({
  words: content.value.trim().split(/\s+/).filter(w => w.length > 0).length,
  chars: content.value.length,
  readTime: Math.ceil(content.value.trim().split(/\s+/).filter(w => w.length > 0).length / 200)
}))
</script>

<template>
  <div class="editor">
    <div class="stats">
      <span>{{ stats.words }} words</span>
      <span>{{ stats.chars }} characters</span>
      <span>~{{ stats.readTime }} min</span>
    </div>
    
    <SuFormField 
      label="Content"
      message="Write your article"
    >
      <template #default="slotProps">
        <SuTextarea 
          placeholder="Start writing..."
          :autoResize="true"
          :minRows="8"
          :maxRows="20"
          :maxLength="5000"
          :showCounter="true"
          v-bind="slotProps"
          v-model="content"
        />
      </template>
    </SuFormField>
  </div>
</template>

SuTextareaField Component ​

For even simpler usage, you can use the SuTextareaField component which automatically combines SuFormField and SuTextarea:

vue
<template>
  <!-- Instead of -->
  <SuFormField label="Description" message="Describe your project">
    <template #default="slotProps">
      <SuTextarea 
        placeholder="Enter your description..."
        v-bind="slotProps"
        v-model="description"
      />
    </template>
  </SuFormField>
  
  <!-- You can simply write -->
  <SuTextareaField 
    label="Description"
    message="Describe your project"
    placeholder="Enter your description..."
    v-model="description"
  />
</template>

The SuTextareaField component accepts all props from both SuFormField and SuTextarea combined, offering a more concise syntax while retaining all features.

See the complete SuTextareaField documentation for more details.

Publié sous licence MIT.