PixelBareTextarea

Unstyled escape-hatch <textarea> passthrough for building custom multi-line inputs without the opinionated PixelTextarea chrome.

  • Zero styling — pure passthrough to the native <textarea> element
  • Takes every native textarea attribute (value, rows, maxlength, etc.)
  • Its element is the native <textarea>: a ref in React, $el in Vue, the textarea[pxlBareTextarea] element in Angular
  • SSR-safe and tree-shakable; no runtime state
  • Ideal for composing bespoke field chrome while keeping native form semantics
Status
stable
Category
forms
Since
v1.0.0

API

import { PixelBareTextarea } from '@pxlkit/ui-kit';

No props of its own.

Also takes the native attributes and event handlers of <textarea> (TextareaHTMLAttributes<HTMLTextAreaElement>).

ref points to <textarea>.

Accessibility

WCAG target: 2.1 AA

  • textbox

Renders a native <textarea> so screen readers announce the textbox role with multiline semantics automatically. Because no chrome is applied, callers MUST supply a visible <label> (for) or an aria-label so the field has an accessible name. Pair with aria-required, aria-invalid, and aria-describedby for validation flows.

Keyboard

KeyActionWhen
TabMoves focus into and out of the textarea—
EnterInserts a newline within the textarea—
Shift+TabMoves focus to the previous focusable element—

Usage

import { PixelBareTextarea } from '@pxlkit/ui-kit';

export function Default() {
  return <PixelBareTextarea placeholder="Write something..." rows={4} />;
}

Examples

Default

import { PixelBareTextarea } from '@pxlkit/ui-kit';

export function Default() {
  return <PixelBareTextarea placeholder="Write something..." rows={4} />;
}

Uncontrolled

import { PixelBareTextarea } from '@pxlkit/ui-kit';

export function Uncontrolled() {
  return (
    <PixelBareTextarea
      defaultValue="Initial draft text"
      rows={4}
      aria-label="Notes"
    />
  );
}

Controlled

import { useState } from 'react';
import { PixelBareTextarea } from '@pxlkit/ui-kit';

export function Controlled() {
  const [value, setValue] = useState('');
  return (
    <div className="flex flex-col gap-2">
      <PixelBareTextarea
        value={value}
        onChange={(e) => setValue(e.target.value)}
        placeholder="Type to see live updates"
        rows={4}
        aria-label="Message"
      />
      <span className="text-xs text-retro-muted">{value.length} chars</span>
    </div>
  );
}

Disabled

import { PixelBareTextarea } from '@pxlkit/ui-kit';

export function Disabled() {
  return (
    <PixelBareTextarea
      disabled
      defaultValue="Cannot edit this field"
      rows={3}
      aria-label="Disabled textarea"
    />
  );
}

Read only

import { PixelBareTextarea } from '@pxlkit/ui-kit';

export function ReadOnly() {
  return (
    <PixelBareTextarea
      readOnly
      defaultValue="Read-only content for reference"
      rows={3}
      aria-label="Read-only textarea"
    />
  );
}

With custom styling

import { PixelBareTextarea } from '@pxlkit/ui-kit';

export function WithCustomStyling() {
  return (
    <PixelBareTextarea
      className="w-full rounded border border-retro-border bg-retro-card p-3 font-mono text-sm text-retro-text"
      placeholder="Escape-hatch: bring your own styles"
      rows={5}
      aria-label="Custom styled textarea"
    />
  );
}

With max length

import { PixelBareTextarea } from '@pxlkit/ui-kit';

export function WithMaxLength() {
  return (
    <PixelBareTextarea
      maxLength={140}
      placeholder="Up to 140 characters"
      rows={3}
      aria-label="Short bio"
    />
  );
}

Required

import { PixelBareTextarea } from '@pxlkit/ui-kit';

export function Required() {
  return (
    <PixelBareTextarea
      required
      placeholder="This field is required"
      rows={3}
      aria-label="Required textarea"
      aria-required="true"
    />
  );
}

Related