Inputs

Auto Grow Textarea

A multiline field that grows with the text up to a set number of rows and scrolls after that. CSS holds the height; there is no React state.

  • textarea
  • auto grow
  • form
  • multiline

Preview

1440pxHost theme

Use it with AI

  1. 1. Copy the link.
  2. 2. Write to your agent in your own words and drop the link into the sentence.
  3. 3. The agent opens the link and installs the component from the registry.

put this in the header: https://vibeui.ru/c/textarea-001?lang=en

One field, grows with the text. Ctrl + Enter to send.
Show the full instructions

What the link says, spelled out. Use it when your agent cannot open links — paste this text instead.

# Install and place "textarea-001" (Auto Grow Textarea) from VibeUI

## 1. Install first — do not skip, do not recreate
Run this exact command before writing any code:
npx shadcn@latest add https://vibeui.ru/r/textarea-001.json

Registry item: https://vibeui.ru/r/textarea-001.json
Installs to: components/vibeui/textarea-001.tsx (the exact path follows this project's components.json aliases).
npm dependencies: none.
Registry dependencies: none.

Install it from the registry. Do not recreate it from the description,
do not substitute a similar component from another library, and do not
rewrite it to match the project's existing style.

## 2. What it is
A multiline field that grows with the text up to a set number of rows and scrolls after that. CSS holds the height; there is no React state.

A multiline field that grows with its content. The height comes from an invisible copy of the text sharing one grid cell, and the input handler only rewrites the wrapper's data-value — no React state, no re-render. It grows up to maxRows and scrolls beyond. Zero dependencies, one file, its own palette.

## 3. How to use it
import { Textarea001 } from "@/components/vibeui/textarea-001"

<Textarea001
  id="brief"
  name="brief"
  label="What needs doing"
  maxRows={12}
/>

Read the installed file for the full prop list.

## 4. Where to place it
This is a small inline component. Put it exactly where the user asked,
inside the existing markup. Do not create a new page, section or
wrapper for it. If a similar control already sits in that spot,
replace it instead of adding a second one.

Placement: ___
(The user fills this line in. If it is still blank, ask where to put it
instead of guessing.)

## 5. Keep exactly as installed
- the local --vibeui-textarea-001-* palette — do not swap it for your theme tokens (bg-background, border-input and the like)
- the growth mechanism: display:grid on the wrapper, ::after with content:attr(data-value) " " and the same grid-area on both copy and textarea — if they drift apart the height stops matching the text
- identical padding, font-size and line-height on the textarea and the copy: the copy has to wrap lines in exactly the same places
- the space inside content: without it the last empty line is not counted and the field jumps on Enter
- resize:none on the textarea — the content sets the size, and the drag handle breaks the layout
- white-space:pre-wrap and word-break:break-word on the copy: otherwise a long word counts as a single line
- the max-height ceiling through --vibeui-textarea-001-max-rows and the scrolling past it
- the <style> block inside the component — it holds the palette, the geometry and the growth mechanism

## 6. You may change
- the label, hint and placeholder copy
- maxRows — how many rows the field shows before it starts scrolling
- the accent through the accent prop — it colours the border and ring on focus
- any native textarea props: name, value, defaultValue, onChange, onKeyDown, required, maxLength, disabled
- width and outer spacing through className

## 7. Rules
- The component is a client one: the "use client" directive is there for onInput. Do not remove it.
- Do not make it controlled with value and no onChange — React blocks typing and the height freezes.
- For a controlled version pass value and onChange: the growth handler runs before your onInput and does not get in its way.
- Do not swap the technique for recalculating scrollHeight in useEffect: that is an extra render per keystroke.
- Do not lift the CSS variables into globals.css: the component has to stay a single file.

## 8. Verify
- it renders with no console errors;
- it looks like the preview on the VibeUI page you copied this from;
- if it does not, you changed something listed in section 5 — put it back.

For developers

npx shadcn@latest add https://vibeui.ru/r/textarea-001.json
https://vibeui.ru/r/textarea-001.json

Компонент самодостаточен: один файл, без зависимостей, собственная палитра в локальных переменных --vibeui-textarea-001-*. Рост держится на приёме с grid: textarea и псевдоэлемент ::after с тем же текстом лежат в одной ячейке, высоту ячейки задаёт копия. Обработчик onInput только переписывает data-value обёртки — React-состояния нет, перерисовки дерева тоже. Потолок задаётся пропом maxRows через переменную --vibeui-textarea-001-max-rows.

Component source

The same file your agent installs. Here in case you would rather copy it by hand.

"use client"

import type { ComponentPropsWithoutRef, CSSProperties } from "react"

export type Textarea001Props = Omit<
  ComponentPropsWithoutRef<"textarea">,
  "rows"
> & {
  label?: string
  /** Строка под полем: подсказка про формат или горячую клавишу. */
  hint?: string
  /** Максимальная высота в строках. Дальше поле начинает прокручиваться. */
  maxRows?: number
  accent?: string
}

// Идея компонента: поле растёт под текст, но не прыгает и не перерисовывает
// React-дерево. Высоту держит невидимая копия текста в той же ячейке grid —
// обработчик только переписывает data-value обёртки, состояния нет.
const STYLES = `
:where([data-vibeui-block="textarea-001"]){
--vibeui-textarea-001-surface:oklch(1 0 0);
--vibeui-textarea-001-surface-border:oklch(0.91 0.006 265);
--vibeui-textarea-001-fg:oklch(0.24 0.016 265);
--vibeui-textarea-001-muted:oklch(0.54 0.014 265);
--vibeui-textarea-001-bg:oklch(1 0 0);
--vibeui-textarea-001-border:oklch(0.87 0.008 265);
--vibeui-textarea-001-accent:oklch(0.55 0.2 262);
--vibeui-textarea-001-radius:0.75rem;
--vibeui-textarea-001-line:1.55;
--vibeui-textarea-001-font:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;
}
/* Собственная подложка: подпись поля — это текст, и на тёмной странице
   он обязан читаться без правки палитры проекта. */
[data-vibeui-block="textarea-001"]{
box-sizing:border-box;padding:0.875rem;
background:var(--vibeui-textarea-001-surface);
border:1px solid var(--vibeui-textarea-001-surface-border);border-radius:0.875rem;
display:flex;flex-direction:column;gap:0.375rem;
font-family:var(--vibeui-textarea-001-font);color:var(--vibeui-textarea-001-fg);
}
[data-vibeui-block="textarea-001"] [data-part="label"]{font-size:0.8125rem;font-weight:500}
/* Поле и его невидимая копия лежат в одной ячейке: высоту задаёт копия. */
[data-vibeui-block="textarea-001"] [data-part="grow"]{
display:grid;
border:1px solid var(--vibeui-textarea-001-border);
border-radius:var(--vibeui-textarea-001-radius);
background:var(--vibeui-textarea-001-bg);
transition:border-color .16s ease,box-shadow .16s ease;
}
[data-vibeui-block="textarea-001"] [data-part="grow"]:focus-within{
border-color:var(--vibeui-textarea-001-accent);
box-shadow:0 0 0 3px color-mix(in oklab,var(--vibeui-textarea-001-accent) 22%,transparent);
}
[data-vibeui-block="textarea-001"] [data-part="grow"]::after{
content:attr(data-value) " ";
visibility:hidden;white-space:pre-wrap;word-break:break-word;
}
[data-vibeui-block="textarea-001"] [data-part="grow"] > textarea,
[data-vibeui-block="textarea-001"] [data-part="grow"]::after{
grid-area:1 / 1 / 2 / 2;
padding:0.75rem 0.875rem;
font:inherit;font-size:0.9375rem;line-height:var(--vibeui-textarea-001-line);
}
[data-vibeui-block="textarea-001"] textarea{
margin:0;border:0;outline:none;resize:none;overflow:auto;
background:transparent;color:inherit;
min-height:calc(3 * var(--vibeui-textarea-001-line) * 0.9375rem + 1.5rem);
max-height:calc(var(--vibeui-textarea-001-max-rows,10) * var(--vibeui-textarea-001-line) * 0.9375rem + 1.5rem);
}
[data-vibeui-block="textarea-001"] textarea::placeholder{color:color-mix(in oklab,var(--vibeui-textarea-001-muted) 70%,transparent)}
[data-vibeui-block="textarea-001"] textarea:disabled{cursor:not-allowed}
[data-vibeui-block="textarea-001"]:has(textarea:disabled) [data-part="grow"]{opacity:.55}
[data-vibeui-block="textarea-001"] [data-part="hint"]{font-size:0.75rem;color:var(--vibeui-textarea-001-muted)}
@media (prefers-reduced-motion:reduce){[data-vibeui-block="textarea-001"] *{animation:none!important;transition:none!important}}
`

/**
 * Текстовое поле, которое растёт под содержимое без пересчёта в React.
 * Один файл, ноль зависимостей, собственная палитра.
 */
export function Textarea001({
  label = "Что нужно сделать",
  hint = "Одно поле, растёт под текст. Ctrl + Enter — отправить.",
  maxRows = 10,
  accent,
  id,
  className,
  style,
  defaultValue,
  onInput,
  placeholder = "Опишите задачу своими словами",
  ...props
}: Textarea001Props) {
  const palette = {
    "--vibeui-textarea-001-max-rows": maxRows,
    ...(accent ? { "--vibeui-textarea-001-accent": accent } : null),
    ...style,
  } as CSSProperties

  return (
    <>
      <style href="vibeui-textarea-001" precedence="medium">
        {STYLES}
      </style>
      <div
        data-vibeui-block="textarea-001"
        className={className}
        style={palette}
      >
        {label ? (
          <label data-part="label" htmlFor={id}>
            {label}
          </label>
        ) : null}
        <div data-part="grow" data-value={String(defaultValue ?? "")}>
          <textarea
            {...props}
            id={id}
            rows={3}
            placeholder={placeholder}
            defaultValue={defaultValue}
            onInput={(event) => {
              const grow = event.currentTarget.parentElement

              if (grow) {
                grow.dataset.value = event.currentTarget.value
              }

              onInput?.(event)
            }}
          />
        </div>
        {hint ? <span data-part="hint">{hint}</span> : null}
      </div>
    </>
  )
}