Skip to content

UITextarea

UITextarea

A versatile textarea component that provides multi-line text input capabilities with features like auto-resize, character count, validation, and custom styling.

<template>
<UITextarea
v-model="text"
:placeholder="'Enter your message...'"
:rows="4"
:maxlength="500"
:autosize="true"
:show-count="true"
@change="handleChange"
>
<template #label>
Message
</template>
<template #hint>
Maximum 500 characters
</template>
</UITextarea>
</template>
<script setup lang="ts">
const text = ref('')
const handleChange = (value: string) => {
console.log('Text changed:', value)
}
</script>

Props

  • modelValue (string): Input value
  • placeholder (string): Placeholder text
  • rows (number): Initial number of rows
  • maxlength (number): Maximum characters
  • minlength (number): Minimum characters
  • autosize (boolean): Auto-resize height
  • showCount (boolean): Show character count
  • disabled (boolean): Disable input
  • readonly (boolean): Read-only mode
  • error (boolean): Error state
  • required (boolean): Required field
  • name (string): Input name
  • label (string): Input label
  • hint (string): Helper text

Events

  • update:modelValue: Value updated
  • change: Value changed
  • input: Input event
  • focus: Input focused
  • blur: Input blurred
  • keydown: Key pressed
  • keyup: Key released

Slots

  • label: Custom label content
  • hint: Custom hint content
  • prefix: Content before input
  • suffix: Content after input
  • count: Custom character count

Usage Examples

  1. Basic Textarea:
<template>
<UITextarea
v-model="comment"
placeholder="Write a comment..."
rows="3"
/>
</template>
<script setup>
const comment = ref('')
</script>
  1. Form Field with Validation:
<template>
<form @submit.prevent="handleSubmit">
<UITextarea
v-model="feedback"
:error="errors.feedback"
:required="true"
:minlength="10"
:maxlength="1000"
:show-count="true"
class="mb-4"
>
<template #label>
<span class="flex items-center space-x-2">
<span>Feedback</span>
<InfoIcon
class="w-4 h-4 text-gray-400"
v-tooltip="'Please provide detailed feedback'"
/>
</span>
</template>
<template #hint>
<span
:class="[
'text-sm',
errors.feedback ? 'text-error-600' : 'text-gray-500'
]"
>
{{ errors.feedback || 'Minimum 10 characters' }}
</span>
</template>
<template #count="{ current, max }">
<span class="text-sm text-gray-500">
{{ current }}/{{ max }} characters
</span>
</template>
</UITextarea>
<UIButton
type="submit"
:disabled="!isValid"
>
Submit Feedback
</UIButton>
</form>
</template>
<script setup>
const feedback = ref('')
const errors = reactive({
feedback: ''
})
const isValid = computed(() => {
return feedback.value.length >= 10 &&
feedback.value.length <= 1000 &&
!errors.feedback
})
const validateFeedback = () => {
if (!feedback.value) {
errors.feedback = 'Feedback is required'
} else if (feedback.value.length < 10) {
errors.feedback = 'Feedback must be at least 10 characters'
} else {
errors.feedback = ''
}
}
const handleSubmit = () => {
validateFeedback()
if (isValid.value) {
// Submit form
}
}
</script>
  1. Rich Text Input:
<template>
<div class="space-y-4">
<UITextarea
v-model="content"
:autosize="true"
:rows="6"
class="editor"
>
<template #label>
Content
</template>
<template #prefix>
<div class="flex items-center space-x-2 p-2 border-b">
<UIButton
v-for="format in formats"
:key="format.id"
size="sm"
:variant="isFormatActive(format.id) ? 'primary' : 'default'"
@click="toggleFormat(format.id)"
>
<component :is="format.icon" class="w-4 h-4" />
</UIButton>
</div>
</template>
</UITextarea>
<div class="flex justify-between items-center">
<UIButton
size="sm"
@click="clearContent"
>
Clear
</UIButton>
<div class="text-sm text-gray-500">
{{ wordCount }} words
</div>
</div>
</div>
</template>
<script setup>
import {
BoldIcon,
ItalicIcon,
UnderlineIcon,
ListIcon
} from '@gohighlevel/ghl-icons/24/outline'
const content = ref('')
const activeFormats = ref(new Set())
const formats = [
{ id: 'bold', icon: BoldIcon },
{ id: 'italic', icon: ItalicIcon },
{ id: 'underline', icon: UnderlineIcon },
{ id: 'list', icon: ListIcon }
]
const wordCount = computed(() => {
return content.value.trim().split(/\s+/).length
})
const isFormatActive = (format) => {
return activeFormats.value.has(format)
}
const toggleFormat = (format) => {
if (activeFormats.value.has(format)) {
activeFormats.value.delete(format)
} else {
activeFormats.value.add(format)
}
}
const clearContent = () => {
content.value = ''
activeFormats.value.clear()
}
</script>
<style>
.editor {
@apply font-mono;
}
</style>

Best Practices

  1. User Experience:

    • Clear placeholder
    • Appropriate sizing
    • Visual feedback
    • Error states
  2. Accessibility:

    • ARIA labels
    • Focus states
    • Error messages
    • Keyboard support
  3. Validation:

    • Input constraints
    • Error handling
    • Clear feedback
    • Real-time validation
  4. Performance:

    • Debounce updates
    • Optimize resizing
    • Memory management
    • Event handling

Common Use Cases

  1. Comment Form:
<template>
<div class="comment-form space-y-4">
<UITextarea
v-model="comment"
:placeholder="'Write a comment...'"
:autosize="true"
:minlength="2"
:maxlength="500"
:show-count="true"
class="w-full"
>
<template #suffix>
<div class="flex items-center space-x-2 p-2">
<UIButton
size="sm"
:disabled="!isValid"
@click="submitComment"
>
Post
</UIButton>
</div>
</template>
</UITextarea>
</div>
</template>
  1. Bio Editor:
<template>
<div class="bio-editor">
<UITextarea
v-model="profile.bio"
:placeholder="'Tell us about yourself...'"
:maxlength="160"
:show-count="true"
:autosize="true"
>
<template #label>
Bio
</template>
<template #hint>
<span class="text-sm text-gray-500">
This will appear on your public profile
</span>
</template>
<template #count="{ current, max }">
<span
:class="[
'text-sm',
current > max * 0.9 ? 'text-warning-600' : 'text-gray-500'
]"
>
{{ current }}/{{ max }}
</span>
</template>
</UITextarea>
</div>
</template>
  1. Message Composer:
<template>
<div class="message-composer">
<UITextarea
v-model="message"
:placeholder="'Type your message...'"
:autosize="true"
:maxlength="2000"
class="composer"
@keydown.enter.prevent="sendMessage"
>
<template #prefix>
<div class="flex items-center space-x-2 p-2">
<UIButton
v-for="action in actions"
:key="action.id"
size="sm"
variant="ghost"
@click="action.handler"
>
<component :is="action.icon" class="w-5 h-5" />
</UIButton>
</div>
</template>
<template #suffix>
<div class="flex items-center p-2">
<UIButton
type="primary"
:disabled="!message.trim()"
@click="sendMessage"
>
Send
</UIButton>
</div>
</template>
</UITextarea>
</div>
</template>
<script setup>
import {
ImageIcon,
AttachmentIcon,
EmojiIcon
} from '@gohighlevel/ghl-icons/24/outline'
const message = ref('')
const actions = [
{
id: 'image',
icon: ImageIcon,
handler: () => attachImage()
},
{
id: 'file',
icon: AttachmentIcon,
handler: () => attachFile()
},
{
id: 'emoji',
icon: EmojiIcon,
handler: () => toggleEmojiPicker()
}
]
const sendMessage = () => {
if (!message.value.trim()) return
// Send message logic
message.value = ''
}
</script>
<style>
.composer {
@apply rounded-lg border-gray-200;
}
.composer:focus-within {
@apply border-primary-500 ring-1 ring-primary-500;
}
</style>