Component
Textarea With Error
Textarea with a destructive error state and an associated inline message (aria-invalid + role=alert).
- Component
- React
- TSX
- Tailwind CSS
- MIT
Preview
Live component
9:41 100%
devsnips.dev/library/react/components/textareas/textarea-with-error/ fluid · no fixed width 100%
Install
Add to your project
npx devsnips add React/Components/Textareas/textarea-with-error React/Components/Textareas/textarea-with-error import type { ReactNode, TextareaHTMLAttributes } from "react";
import { useId } from "react";
function cx(...parts: Array<string | false | null | undefined>): string {
return parts.filter(Boolean).join(" ");
}
const TEXTAREA_BASE =
"w-full min-h-[80px] resize-y rounded-[var(--ds-radius-sm)] border bg-[var(--ds-color-input)] px-3 py-2 text-sm leading-5 text-[var(--ds-color-foreground)] shadow-none transition-colors duration-150 ease-out placeholder:text-[var(--ds-color-muted-foreground)] hover:bg-[var(--ds-color-input-hover,var(--ds-color-input))] focus:bg-[var(--ds-color-input-focus,var(--ds-color-input))] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--ds-color-focus-ring)] disabled:pointer-events-none disabled:bg-[var(--ds-color-muted)] disabled:text-[var(--ds-color-muted-foreground)] disabled:opacity-60 read-only:bg-[var(--ds-color-surface-subtle)] read-only:text-[var(--ds-color-muted-foreground)] motion-reduce:transition-none";
export interface TextareaWithErrorProps extends TextareaHTMLAttributes<HTMLTextAreaElement> {
/** Visible label — required. */
label: ReactNode;
/** Error message. When set, the field enters the error state. */
error?: string;
}
/**
* Textarea with an inline error state. Passing `error` switches the border
* to color.destructive, sets `aria-invalid="true"`, and renders the message
* in a `role="alert"` region linked with `aria-describedby` — the state is
* never communicated by color alone. Clear `error` (set it back to
* `undefined`) once the value is valid again.
*/
export function TextareaWithError({ label, error, id, className, rows = 3, required, ...props }: TextareaWithErrorProps) {
const generatedId = useId();
const textareaId = id ?? `textarea-${generatedId}`;
const errorId = `${textareaId}-error`;
const hasError = Boolean(error);
return (
<div className="w-full">
<label
htmlFor={textareaId}
className="mb-2 block text-[13px] font-medium leading-5 text-[var(--ds-color-foreground)]"
>
{label}
{required ? (
<span aria-hidden="true" className="ml-0.5 text-[var(--ds-color-destructive)]">*</span>
) : null}
</label>
<textarea
id={textareaId}
rows={rows}
required={required}
aria-invalid={hasError ? true : undefined}
aria-describedby={hasError ? errorId : undefined}
className={cx(
TEXTAREA_BASE,
hasError
? "border-[var(--ds-color-destructive)]"
: "border-[var(--ds-color-border)] focus:border-[var(--ds-color-border-strong)]",
className,
)}
{...props}
/>
{hasError ? (
<p id={errorId} role="alert" className="mt-2 text-xs leading-4 text-[var(--ds-color-destructive)]">
Error: {error}
</p>
) : null}
</div>
);
}
export default TextareaWithError; /* DevSnips React — JavaScript parity build.
* Same API, behavior, and classes as code.tsx; TypeScript types removed.
* Regenerated from code.tsx — edit code.tsx and re-run the generator.
*/
import { useId } from "react";
function cx(...parts) {
return parts.filter(Boolean).join(" ");
}
const TEXTAREA_BASE = "w-full min-h-[80px] resize-y rounded-[var(--ds-radius-sm)] border bg-[var(--ds-color-input)] px-3 py-2 text-sm leading-5 text-[var(--ds-color-foreground)] shadow-none transition-colors duration-150 ease-out placeholder:text-[var(--ds-color-muted-foreground)] hover:bg-[var(--ds-color-input-hover,var(--ds-color-input))] focus:bg-[var(--ds-color-input-focus,var(--ds-color-input))] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--ds-color-focus-ring)] disabled:pointer-events-none disabled:bg-[var(--ds-color-muted)] disabled:text-[var(--ds-color-muted-foreground)] disabled:opacity-60 read-only:bg-[var(--ds-color-surface-subtle)] read-only:text-[var(--ds-color-muted-foreground)] motion-reduce:transition-none";
export function TextareaWithError({ label, error, id, className, rows = 3, required, ...props }) {
const generatedId = useId();
const textareaId = id ?? `textarea-${generatedId}`;
const errorId = `${textareaId}-error`;
const hasError = Boolean(error);
return <div className="w-full">
<label
htmlFor={textareaId}
className="mb-2 block text-[13px] font-medium leading-5 text-[var(--ds-color-foreground)]"
>
{label}
{required ? <span aria-hidden="true" className="ml-0.5 text-[var(--ds-color-destructive)]">*</span> : null}
</label>
<textarea
id={textareaId}
rows={rows}
required={required}
aria-invalid={hasError ? true : undefined}
aria-describedby={hasError ? errorId : undefined}
className={cx(
TEXTAREA_BASE,
hasError ? "border-[var(--ds-color-destructive)]" : "border-[var(--ds-color-border)] focus:border-[var(--ds-color-border-strong)]",
className
)}
{...props}
/>
{hasError ? <p id={errorId} role="alert" className="mt-2 text-xs leading-4 text-[var(--ds-color-destructive)]">
Error: {error}
</p> : null}
</div>;
}
export default TextareaWithError; # Textarea With Error
Textarea with a destructive error state and an associated inline message (aria-invalid + role=alert).
## Usage
```tsx
<TextareaWithError label="Bug report" error={message} rows={5} />
```
## JavaScript
A `code.jsx` build is provided for projects that ship plain JSX. It exposes the same API and behavior as `code.tsx` — only the TypeScript types are removed.
```jsx
<TextareaWithError label="Bug report" error={message} rows={5} />
```
## Props
| Name | Type | Default | Description |
|---|---|---:|---|
| `label` | `ReactNode` (required) | — | Visible label above the control. |
| `value` / `defaultValue` | `string` | — | Controlled / uncontrolled value. |
| `onChange` | `(event) => void` | — | Native change callback. |
| `rows` | `number` | `3` | Visible rows — the natural height floor (with `min-h-[80px]`). |
| `placeholder` | `string` | — | Muted placeholder (never critical information). |
| `disabled` | `boolean` | — | Native disabled — not focusable, not submitted. |
| `readOnly` | `boolean` | — | Native read-only — focusable, selectable, submitted. |
| `required` / `name` / `id` | `boolean` / `string` | — | Native form semantics (`id` also the label `htmlFor`). |
| `minLength` / `maxLength` | `number` | — | Native length constraints. |
| `className` | `string` | — | Extra Tailwind classes merged onto the control. |
| other native props / `aria-*` | — | — | Passed through to the `<textarea>`. |
| `error` | `string` | — | Error message. When set: destructive border + `aria-invalid` + `role="alert"` message. |
## States
Native textarea with an inline error state driven by the `error` prop. While `error` is set the border switches to `color.destructive`, `aria-invalid="true"` is applied, and the message renders below the field in a `role="alert"` region linked with `aria-describedby`. Clearing `error` returns the field to the default state. The state is never color alone — the message text carries it.
## Accessibility
`aria-invalid="true"` on the textarea while invalid; the message is a `role="alert"` region associated with `aria-describedby`, so screen readers announce it when it appears. The visible message (`Error: …`) plus border means the state does not rely on red color alone. Visible `focus-visible` ring.
## Styling
Built with React, Tailwind CSS, and DevSnips design tokens. The component consumes the `--ds-*` semantic tokens via arbitrary values (for example `bg-[var(--ds-color-input)]`). Define the tokens once in your theme — no component-specific CSS file is required.
## Design Tokens
See [React/DESIGN_TOKENS.md](../../../DESIGN_TOKENS.md) for the authoritative token specification. This textarea uses the semantic color, radius, spacing, and motion tokens.
## Notes
Keep the message specific and actionable (what is wrong + how to fix it). Validate on submit or blur — showing an error while the user is still typing their first characters is noise. 66 lines UTF-8 · LF · Spaces: 2
Continue browsing