TextArea
The TextArea component provides a multi-line text input with character counting, labels, hints, and automatic height adjustment. It’s ideal for longer text content like messages, descriptions, and comments.
Import
Section titled “Import”import { TextArea, type InputRef } from '@space-uy/pulsar-ui';Basic usage
Section titled “Basic usage”const [message, setMessage] = useState('');
<TextArea  label="Message"  placeholder="Enter your message"  value={message}  onChangeText={setMessage}/>;Properties
Section titled “Properties”| Property | Type | Required | Default value | Description | 
|---|---|---|---|---|
| label | string | ❌ | - | Label text displayed above the textarea | 
| hint | string | ❌ | - | Hint text displayed below the textarea | 
| error | boolean | ❌ | false | Whether the textarea is in error state | 
| numberOfLines | number | ❌ | 4 | Number of visible lines in the textarea | 
| maxLength | number | ❌ | 1200 | Maximum number of characters allowed | 
| onChangeText | (text: string) => void | ❌ | - | Callback when textarea text changes | 
| editable | boolean | ❌ | true | Whether the textarea is editable | 
| style | StyleProp<ViewStyle> | ❌ | - | Custom styles for the textarea container | 
| ...rest | TextInputProps | ❌ | - | Additional TextInput props | 
InputRef Methods
Section titled “InputRef Methods”When using a ref, the following methods are available:
| Method | Type | Description | 
|---|---|---|
| focus | () => void | Focus the textarea | 
| blur | () => void | Remove focus from textarea | 
Basic examples
Section titled “Basic examples”Simple textarea
Section titled “Simple textarea”const [comment, setComment] = useState('');
<TextArea  label="Comment"  placeholder="Share your thoughts..."  value={comment}  onChangeText={setComment}  numberOfLines={3}/>;Textarea with character limit
Section titled “Textarea with character limit”const [description, setDescription] = useState('');
<TextArea  label="Description"  placeholder="Describe your project..."  value={description}  onChangeText={setDescription}  maxLength={500}  numberOfLines={5}  hint="Provide a detailed description of your project"/>;Error state textarea
Section titled “Error state textarea”const [feedback, setFeedback] = useState('');const [hasError, setHasError] = useState(false);
<TextArea  label="Feedback"  placeholder="Your feedback is important to us"  value={feedback}  onChangeText={(text) => {    setFeedback(text);    setHasError(text.length < 10);  }}  error={hasError}  hint={    hasError      ? 'Feedback must be at least 10 characters long'      : 'Help us improve our service'  }  numberOfLines={4}/>;Advanced examples
Section titled “Advanced examples”Contact form
Section titled “Contact form”const [formData, setFormData] = useState({  name: '',  email: '',  subject: '',  message: '',});
const [errors, setErrors] = useState<Record<string, boolean>>({});
const validateForm = () => {  const newErrors: Record<string, boolean> = {};  newErrors.name = formData.name.length < 2;  newErrors.email = !formData.email.includes('@');  newErrors.subject = formData.subject.length < 5;  newErrors.message = formData.message.length < 20;  setErrors(newErrors);  return !Object.values(newErrors).some(Boolean);};
<Card>  <Text variant="h3">Contact Us</Text>  <Text variant="ps" style={{ opacity: 0.7, marginTop: 4 }}>    We'd love to hear from you  </Text>
  <View style={{ gap: 16, marginTop: 20 }}>    <Input      label="Name"      value={formData.name}      onChangeText={(name) => setFormData((prev) => ({ ...prev, name }))}      error={errors.name}      hint={errors.name ? 'Name must be at least 2 characters' : ''}      placeholder="Your full name"    />
    <Input      label="Email"      value={formData.email}      onChangeText={(email) => setFormData((prev) => ({ ...prev, email }))}      error={errors.email}      hint={errors.email ? 'Please enter a valid email address' : ''}      placeholder="your@email.com"      keyboardType="email-address"    />
    <Input      label="Subject"      value={formData.subject}      onChangeText={(subject) => setFormData((prev) => ({ ...prev, subject }))}      error={errors.subject}      hint={errors.subject ? 'Subject must be at least 5 characters' : ''}      placeholder="What is this about?"    />
    <TextArea      label="Message"      value={formData.message}      onChangeText={(message) => setFormData((prev) => ({ ...prev, message }))}      error={errors.message}      hint={        errors.message          ? 'Message must be at least 20 characters'          : 'Tell us more about your inquiry'      }      placeholder="Your message here..."      numberOfLines={6}      maxLength={2000}    />
    <Button      text="Send Message"      onPress={validateForm}      disabled={Object.values(formData).some((value) => !value.trim())}    />  </View></Card>;Review form
Section titled “Review form”const [review, setReview] = useState({  rating: 5,  title: '',  comment: '',});
<Card>  <Text variant="h3">Write a Review</Text>
  <View style={{ gap: 16, marginTop: 16 }}>    <View>      <Text variant="h5" style={{ marginBottom: 8 }}>        Rating      </Text>      <View style={{ flexDirection: 'row', gap: 4 }}>        {[1, 2, 3, 4, 5].map((star) => (          <Pressable            key={star}            onPress={() => setReview((prev) => ({ ...prev, rating: star }))}          >            <Icon              name="Star"              size={24}              color={star <= review.rating ? '#FFD700' : colors.border}            />          </Pressable>        ))}      </View>    </View>
    <Input      label="Review Title"      value={review.title}      onChangeText={(title) => setReview((prev) => ({ ...prev, title }))}      placeholder="Summarize your experience"      maxLength={100}    />
    <TextArea      label="Review Comment"      value={review.comment}      onChangeText={(comment) => setReview((prev) => ({ ...prev, comment }))}      placeholder="Share your detailed experience..."      numberOfLines={5}      maxLength={1000}      hint="Help others by sharing specific details about your experience"    />
    <Button      text="Submit Review"      onPress={() => console.log('Review submitted:', review)}      disabled={!review.title.trim() || !review.comment.trim()}    />  </View></Card>;Note-taking app
Section titled “Note-taking app”const [notes, setNotes] = useState([  { id: 1, title: 'Meeting Notes', content: '', lastModified: new Date() },  { id: 2, title: 'Ideas', content: '', lastModified: new Date() },]);
const [selectedNote, setSelectedNote] = useState(notes[0]);
const updateNote = (content: string) => {  const updatedNote = {    ...selectedNote,    content,    lastModified: new Date(),  };  setSelectedNote(updatedNote);  setNotes((prev) =>    prev.map((note) => (note.id === updatedNote.id ? updatedNote : note))  );};
<View style={{ flexDirection: 'row', flex: 1 }}>  {/* Note List */}  <View    style={{ width: 200, borderRightWidth: 1, borderRightColor: colors.border }}  >    <Text variant="h4" style={{ padding: 16 }}>      Notes    </Text>    {notes.map((note) => (      <Pressable        key={note.id}        style={{          padding: 16,          backgroundColor:            selectedNote.id === note.id ? colors.primary + '10' : 'transparent',          borderBottomWidth: 1,          borderBottomColor: colors.border,        }}        onPress={() => setSelectedNote(note)}      >        <Text variant="pm">{note.title}</Text>        <Text variant="ps" style={{ opacity: 0.7, marginTop: 4 }}>          {note.lastModified.toLocaleDateString()}        </Text>      </Pressable>    ))}  </View>
  {/* Note Editor */}  <View style={{ flex: 1, padding: 16 }}>    <Input      value={selectedNote.title}      onChangeText={(title) => setSelectedNote((prev) => ({ ...prev, title }))}      placeholder="Note title"      style={{ marginBottom: 16, fontSize: 18, fontWeight: 'bold' }}    />
    <TextArea      value={selectedNote.content}      onChangeText={updateNote}      placeholder="Start writing your note..."      numberOfLines={20}      maxLength={5000}      style={{ flex: 1 }}    />
    <Text      variant="caption"      style={{ textAlign: 'right', marginTop: 8, opacity: 0.7 }}    >      Last modified: {selectedNote.lastModified.toLocaleString()}    </Text>  </View></View>;Controlled textarea with ref
Section titled “Controlled textarea with ref”const textareaRef = useRef<InputRef>(null);const [content, setContent] = useState('');
const insertTemplate = (template: string) => {  setContent((prev) => prev + template);  textareaRef.current?.focus();};
<View style={{ gap: 16 }}>  <View>    <Text variant="h5" style={{ marginBottom: 8 }}>      Quick Templates    </Text>    <View style={{ flexDirection: 'row', gap: 8, flexWrap: 'wrap' }}>      <Button        text="Thank you"        size="small"        variant="outline"        onPress={() => insertTemplate('Thank you for your time. ')}      />      <Button        text="Best regards"        size="small"        variant="outline"        onPress={() => insertTemplate('\n\nBest regards,\n')}      />      <Button        text="Please advise"        size="small"        variant="outline"        onPress={() => insertTemplate('Please advise. ')}      />    </View>  </View>
  <TextArea    ref={textareaRef}    label="Email Content"    value={content}    onChangeText={setContent}    placeholder="Compose your email..."    numberOfLines={8}    maxLength={3000}  />
  <View style={{ flexDirection: 'row', gap: 8 }}>    <Button text="Clear" variant="outline" onPress={() => setContent('')} />    <Button      text="Focus"      variant="outline"      onPress={() => textareaRef.current?.focus()}    />    <Button text="Send" disabled={!content.trim()} />  </View></View>;Auto-save textarea
Section titled “Auto-save textarea”const [content, setContent] = useState('');const [saveStatus, setSaveStatus] = useState<'saved' | 'saving' | 'unsaved'>(  'saved');
useEffect(() => {  if (content.trim()) {    setSaveStatus('unsaved');    const timer = setTimeout(() => {      setSaveStatus('saving');      // Simulate auto-save      setTimeout(() => {        setSaveStatus('saved');      }, 1000);    }, 2000);
    return () => clearTimeout(timer);  }}, [content]);
<View style={{ gap: 16 }}>  <View    style={{      flexDirection: 'row',      justifyContent: 'space-between',      alignItems: 'center',    }}  >    <Text variant="h4">Document Editor</Text>    <View style={{ flexDirection: 'row', alignItems: 'center', gap: 8 }}>      {saveStatus === 'saving' && <LoadingIndicator size={16} />}      <Text        variant="ps"        style={{          color:            saveStatus === 'saved'              ? 'green'              : saveStatus === 'saving'                ? colors.primary                : 'orange',        }}      >        {saveStatus === 'saved'          ? 'Saved'          : saveStatus === 'saving'            ? 'Saving...'            : 'Unsaved changes'}      </Text>    </View>  </View>
  <TextArea    value={content}    onChangeText={setContent}    placeholder="Start typing your document..."    numberOfLines={15}    maxLength={10000}    hint="Your changes are automatically saved"  /></View>;Readonly textarea
Section titled “Readonly textarea”const termsOfService = `These Terms of Service ("Terms") govern your use of our application and services. By using our services, you agree to these terms.
1. Acceptance of TermsBy accessing and using this service, you accept and agree to be bound by the terms and provision of this agreement.
2. Use LicensePermission is granted to temporarily download one copy of the materials on our website for personal, non-commercial transitory viewing only.
3. DisclaimerThe materials on our website are provided on an 'as is' basis. We make no warranties, expressed or implied, and hereby disclaim and negate all other warranties including without limitation, implied warranties or conditions of merchantability.`;
<Card>  <Text variant="h3">Terms of Service</Text>  <TextArea    value={termsOfService}    editable={false}    numberOfLines={10}    style={{      backgroundColor: colors.altBackground,      marginTop: 16,    }}    hint="Please read our terms of service carefully"  />
  <View style={{ flexDirection: 'row', gap: 12, marginTop: 16 }}>    <Button text="Accept" />    <Button text="Decline" variant="outline" />  </View></Card>;Implementation notes
Section titled “Implementation notes”- Built on top of InputContainer for consistent styling and behavior
- Character counter appears in the bottom-right corner showing current/max characters
- The textarea automatically grows to accommodate the specified number of lines
- Text input starts at the top-left and supports multi-line editing
- Maximum length enforcement prevents typing beyond the specified limit
- Error states change border color and display hint text in error color
- The component handles platform-specific text alignment and behavior
Character counting
Section titled “Character counting”- Character count displays as “current / maximum” in the bottom-right
- Updates in real-time as the user types
- Prevents input when maximum length is reached
- Counter color matches the theme foreground color
Styling
Section titled “Styling”Theme integration
Section titled “Theme integration”The TextArea automatically applies theme styling:
- Background: Uses theme background colors
- Border: Uses theme border colors with focus state changes
- Text: Uses theme typography and foreground colors
- Character counter: Uses caption variant with theme colors
Layout
Section titled “Layout”- Flexible height: Based on numberOfLines * 22px per line
- Text alignment: Top-left aligned for natural writing flow
- Padding: Consistent with other input components
- Counter position: Bottom-right with proper spacing
Accessibility
Section titled “Accessibility”- TextArea fields are fully keyboard accessible
- Labels are properly associated with textarea fields
- Hint text provides additional context for screen readers
- Character limits are announced to assistive technologies
- Error states are communicated to screen readers
- Focus management works correctly with keyboard navigation