Lunia DS

Base components

Textarea

A multi-line text field with the same label, hint and error behavior as Input, which can grow with its text and eases its height so the page below does not jump.

Loading preview

A Description field with autoGrow from 4 to 8 lines, a Feedback field with the resize handle and a 500 character limit in the hint, and an invalid state with "Enter a description of at least 20 characters." Replay types three lines into the Description field so it grows.

Installation

Run this in your app after the one-time setup on the Getting started page.

$ npx @ganisigit/lunia-ds add textarea

Usage

The demo above, as code.

demo.tsx
'use client';

import * as React from 'react';
import { Textarea } from './textarea';

const LINES = [
  'Lunia DS is a motion-first design system.',
  'Every component ships for React and Vue.',
  'Motion tokens keep timing the same everywhere.',
];
const TYPE_MS = 24;
const LIMIT = 500;

export default function TextareaDemo({ replay = 0 }: { replay?: number }) {
  const [description, setDescription] = React.useState('');
  const [feedback, setFeedback] = React.useState('');

  React.useEffect(() => {
    if (replay === 0) return;
    setDescription('');
    // Types three lines, one letter at a time, so the field grows line by line.
    const full = LINES.join('\n');
    const timers = [...full].map((_, i) =>
      window.setTimeout(() => setDescription(full.slice(0, i + 1)), (i + 1) * TYPE_MS),
    );
    return () => {
      for (const t of timers) window.clearTimeout(t);
    };
  }, [replay]);

  return (
    <div className="grid w-full max-w-md gap-6">
      <Textarea
        label="Description"
        required
        autoGrow
        rows={4}
        maxRows={8}
        placeholder="Enter a description..."
        helpText="People see this on your project page."
        value={description}
        onValueChange={setDescription}
        hint="Tell people what this project is about."
      />
      <Textarea
        label="Feedback"
        size="md"
        placeholder="What can we do better?"
        maxLength={LIMIT}
        value={feedback}
        onValueChange={setFeedback}
        // A live count is not a new hint, so it is passed as an element and does not replay the hint entrance.
        hint={<span>{LIMIT - feedback.length} characters left.</span>}
      />
      <Textarea
        label="Summary"
        invalid
        defaultValue="Too short."
        hint="Enter a description of at least 20 characters."
      />
    </div>
  );
}

API

Parts: root (field), label with required mark (*) and help icon, control (native textarea), resize handle, hint text (becomes the error text when invalid).

PropTypeDefaultDescription
valuestringundefinedControlled value.
defaultValuestring""Value when uncontrolled.
size"sm" | "md""sm"Text sm or Text md, with padding to match Input.
rowsnumber4Visible lines before the text scrolls, or the starting height when autoGrow is set.
maxRowsnumber12With autoGrow: the height stops growing at this many lines and the text scrolls.
autoGrowbooleanfalseGrows the height with the text, from rows up to maxRows.
resize"vertical" | "none""vertical"Shows the resize handle. Ignored when autoGrow is set.
labelstringundefinedLabel above the field. Without it, pass aria-label.
placeholderstringundefinedExample text shown while empty.
hintstringundefinedHelp line under the field. When invalid, pass the error text here.
helpTextstringundefinedShows the help icon in the label row (Figma 83:2087) with this text in a tooltip.
requiredbooleanfalseNative required; shows the * after the label.
invalidbooleanfalseError border and a red hint (Figma Destructive).
disabledbooleanfalseTurns the field off.
namestringundefinedForm field name.
EventDescription
valueChangeThe value changed while typing. Payload: the new value.
SlotDescription
hintRich hint content, replaces the hint prop.

Motion

Every row names a token from the Motion page. The last column says what happens when the user asks for reduced motion.

IDTriggerElementPropertyFrom → toTimingReduced motion
TXA1hover the fieldcontrol borderborder-colorborder-primary → border-control_hoverduration-sm (200)ease-outSwaps at once
TXA2focuscontrolborder-color, box-shadowborder-primary, shadow-xs → border-brand, focus-ring-shadow-xsduration-2xs (120)ease-outShows at once
TXA3invalid -> truecontrol border, hintborder-color, colorborder-primary, text-tertiary → border-error, text-error-primaryduration-sm (200)ease-outSwaps at once
TXA4hint text changes (for example to an error)hintopacity, translate y0, -distance-rise → 1, 0duration-sm (200)ease-outSwaps at once
TXA5autoGrow: a line is added or removedcontrolheightold content height → new content height (between rows and maxRows)duration-2xs (120)ease-outResizes at once

Accessibility

  • A native textarea inside the same field primitive as Input: label, hint and aria-invalid are linked the same way.
  • The resize handle is the native one, so it works with the pointer only; autoGrow is the keyboard-friendly choice for long text.
  • Height changes never move the caret or the scroll position inside the field.
  • Reduced motion: the height changes at once.