Skip to content

Early previewhamur is not on npm yet. The components are being built in the open, follow along on GitHub.

InputsReact

Textarea Autosize

A native textarea that grows with its text: one line at first, a line more for each line typed, pasted or wrapped, up to maxRows, after which it scrolls.

Rendered by React 19.3.1

Loading React…
import { Field } from '@hamur/react/field'
import { TextareaAutosize } from '@hamur/react/textarea-autosize'

export default function TextareaAutosizeExample() {
  return (
    <Field.Root name="message" className="field">
      <Field.Label>Message</Field.Label>
      <TextareaAutosize
        className="textarea-autosize"
        minRows={2}
        maxRows={6}
        placeholder="Write a message"
      />
      <Field.Description className="muted">
        Grows from 2 to 6 lines, then scrolls.
      </Field.Description>
    </Field.Root>
  )
}
Not measured yetTests passing in React
Not measured yetSize
Not measured yetMemory
Not measured yetSpeed
[N] commits

Installation

npm i @hamur/react

Not on npm yet. The package goes out under the chosen name.

Anatomy

Import the component and assemble its parts:

<TextareaAutosize />

Examples

Controlled

A bio held by the app, filled and cleared by buttons, with the characters counted in Field.Description.

TextareaAutosizeControlled.tsx
import * as React from 'react'
import { Field } from '@hamur/react/field'
import { TextareaAutosize } from '@hamur/react/textarea-autosize'

const BIO =
  'Product designer in Lisbon.\nI draw interfaces.\nOff screen: tiles and trams.\nAsk me about type.'

export default function TextareaAutosizeControlledExample() {
  const [value, setValue] = React.useState('')
  return (
    <div className="stack">
      <Field.Root name="bio" className="field">
        <Field.Label>Bio</Field.Label>
        <TextareaAutosize
          className="textarea-autosize"
          value={value}
          minRows={2}
          maxRows={8}
          maxLength={280}
          onValueChange={(next: string) => setValue(next)}
        />
        <Field.Description className="muted">{value.length} of 280 characters</Field.Description>
      </Field.Root>
      <div className="row">
        <button className="button" type="button" onClick={() => setValue(BIO)}>
          Fill
        </button>
        <button className="button" type="button" onClick={() => setValue('')}>
          Clear
        </button>
      </div>
    </div>
  )
}

Chat

A composer that grows to four lines under a message list, kept scrolled down from onHeightChange.

TextareaAutosizeChat.tsx
import * as React from 'react'
import { TextareaAutosize } from '@hamur/react/textarea-autosize'

export default function TextareaAutosizeChatExample() {
  const [messages, setMessages] = React.useState([
    'Is the build green?',
    'Yes, all three browsers.',
  ])
  const [draft, setDraft] = React.useState('')
  const [height, setHeight] = React.useState(0)
  const list = React.useRef<HTMLUListElement>(null)
  const scrollDown = () => list.current?.scrollTo({ top: list.current.scrollHeight })
  function send() {
    if (!draft.trim()) return
    setMessages([...messages, draft])
    setDraft('')
    requestAnimationFrame(scrollDown)
  }
  return (
    <div className="stack">
      <div className="textarea-autosize-chat">
        <ul ref={list} className="textarea-autosize-chat-list" aria-label="Messages">
          {messages.map((message, index) => (
            <li key={index} className="textarea-autosize-chat-message">
              {message}
            </li>
          ))}
        </ul>
        <div className="textarea-autosize-chat-composer">
          <TextareaAutosize
            className="textarea-autosize"
            aria-label="Message"
            placeholder="Write a message"
            value={draft}
            maxRows={4}
            onValueChange={(next: string) => setDraft(next)}
            onHeightChange={(next: number) => { setHeight(next) scrollDown() }}
          />
          <button className="button" type="button" onClick={send}>
            Send
          </button>
        </div>
      </div>
      <p className="muted textarea-autosize-height">Composer height: {Math.round(height)} px</p>
    </div>
  )
}

Actions

A template written to the element from script, sized by updateHeight() through actionsRef.

TextareaAutosizeActions.tsx
import * as React from 'react'
import { Field } from '@hamur/react/field'
import { TextareaAutosize } from '@hamur/react/textarea-autosize'

const TEMPLATE = 'Steps to reproduce:\n1.\n2.\nExpected:\nActual:'

export default function TextareaAutosizeActionsExample() {
  const textarea = React.useRef<HTMLTextAreaElement>(null)
  const actions = React.useRef<TextareaAutosize.Actions>(null)
  function insert() {
    if (!textarea.current) return
    textarea.current.value = TEMPLATE
    actions.current?.updateHeight()
  }
  return (
    <div className="stack">
      <Field.Root name="report" className="field">
        <Field.Label>Bug report</Field.Label>
        <TextareaAutosize
          ref={textarea}
          actionsRef={actions}
          className="textarea-autosize"
          minRows={2}
        />
        <Field.Description className="muted">
          The template is written from script, so updateHeight() sizes the field for it.
        </Field.Description>
      </Field.Root>
      <div className="row">
        <button className="button" type="button" onClick={insert}>
          Insert template
        </button>
      </div>
    </div>
  )
}

Form

Required feedback with a default text and minRows 3 in a form, a message when it is empty, and a reset.

TextareaAutosizeForm.tsx
import * as React from 'react'
import { Field } from '@hamur/react/field'
import { Form } from '@hamur/react/form'
import { TextareaAutosize } from '@hamur/react/textarea-autosize'

const FEEDBACK = 'Fast checkout.\nClear prices.'

export default function TextareaAutosizeFormExample() {
  const [submitted, setSubmitted] = React.useState('')
  return (
    <Form
      className="stack"
      onFormSubmit={(_values, details) => {
        const form = details.event.target as HTMLFormElement
        setSubmitted([...new FormData(form)].map(([key, value]) => `${key}=${value}`).join('&'))
      }}
    >
      <Field.Root name="feedback" className="field">
        <Field.Label>Feedback</Field.Label>
        <TextareaAutosize
          className="textarea-autosize"
          defaultValue={FEEDBACK}
          minRows={3}
          maxRows={8}
          required
        />
        <Field.Error className="error" match="valueMissing">
          Write some feedback.
        </Field.Error>
      </Field.Root>
      <div className="row">
        <button className="button" type="submit">
          Send
        </button>
        <button className="button" type="reset" onClick={() => setSubmitted('')}>
          Reset
        </button>
      </div>
      {submitted ? <pre className="output">{submitted}</pre> : null}
    </Form>
  )
}

API reference

7 props across 1 parts, in React syntax. className, style, render are listed once, under Shared props.

TextareaAutosizeTextareaAutosize

A native textarea that grows with its text between minRows and maxRows and works with Field. Renders a <textarea>.

TextareaAutosize props
PropTypeDefault
minRowsThe smallest height, in rows.number1
maxRowsThe largest height, in rows.numberInfinity
onHeightChangeCallback fired with the border-box height in CSS pixels after mount and after each change.((height: number) => void)—
actionsRefA ref to imperative actions.React.RefObject<TextareaAutosizeActions | null>—
valueThe value of the textarea.string | number | readonly string[]—
defaultValueThe default value of the textarea.string | number | readonly string[]—
onValueChangeCallback fired when the value changes.((value: string, eventDetails: { reason: "none"; event: Event; cancel: () => void; allowPropagation: () => void; isCanceled: boolean; isPropagationAllowed: boolean; trigger: Element | undefined; }) => void)—
TextareaAutosize data attributes
Data attributeDescription
data-disabledPresent when the textarea is disabled.
data-validPresent when the textarea is in a valid state (when wrapped in Field.Root).
data-invalidPresent when the textarea is in an invalid state (when wrapped in Field.Root).
data-touchedPresent when the textarea has been touched (when wrapped in Field.Root).
data-dirtyPresent when the textarea's value has changed (when wrapped in Field.Root).
data-filledPresent when the textarea is filled (when wrapped in Field.Root).
data-focusedPresent when the textarea is focused (when wrapped in Field.Root).

Shared props

Every part that renders an element takes these.

Shared props
PropType
classNameCSS class applied to the element, or a function that returns a class based on the component's state.string | ((state: State) => string | undefined)
styleStyle applied to the element, or a function that returns a style object based on the component's state.React.CSSProperties | ((state: State) => React.CSSProperties | undefined)
renderAllows you to replace the component's HTML element with a different tag, or compose it with another component.React.ReactElement<unknown, string | React.JSXElementConstructor<any>> | ComponentRenderFn<HTMLProps, State>

Keyboard and accessibility

Keyboard

TextareaAutosize is a native <textarea>; the browser handles its keys. Enter inserts a line break and never submits the form.

Accessibility

What the parts set
PartSetsSo that
TextareaAutosizerenders a <textarea> with no role and no aria-multiline attributeit is read as the multi-line text field it is
TextareaAutosizethe label, aria-describedby and aria-invalid of the Field around itthe textarea is read with its name, hint and error
TextareaAutosizerows from the initial text, then only height, min-height and max-heightit never writes a width, an outline or a padding the app set

Focus order

  1. TextareaAutosize is one tab stop.