Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions apps/www/src/content/docs/components/textarea/demo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,16 @@ export const playground = {
type: 'checkbox',
defaultValue: false
},
size: {
type: 'select',
options: ['large', 'small'],
defaultValue: 'large'
},
variant: {
type: 'select',
options: ['default', 'borderless'],
defaultValue: 'default'
},
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lets add rows prop control as well in the playground.

width: {
type: 'text',
defaultValue: '400px'
Expand Down Expand Up @@ -55,6 +65,30 @@ export const controlledDemo = {
}`
};

export const sizeDemo = {
type: 'code',
code: `<Flex direction="column" gap="medium" style={{ width: 400 }}>
<TextArea placeholder="Large size (default)" />
<TextArea placeholder="Small size" size="small" />
</Flex>`
};

export const variantDemo = {
type: 'code',
code: `<Flex direction="column" gap="medium" style={{ width: 400 }}>
<TextArea placeholder="Default variant" />
<TextArea placeholder="Borderless variant" variant="borderless" />
</Flex>`
};

export const rowsDemo = {
type: 'code',
code: `<Flex direction="column" gap="medium" style={{ width: 400 }}>
<TextArea placeholder="Default (3 rows)" />
<TextArea placeholder="6 rows" rows={6} />
</Flex>`
};

export const widthDemo = {
type: 'code',
code: `<TextArea
Expand Down
24 changes: 23 additions & 1 deletion apps/www/src/content/docs/components/textarea/index.mdx
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
---
title: TextArea
description: A multi-line text input field.
description: A multi-line text input field with size and variant options.
source: packages/raystack/components/text-area
---

import {
playground,
basicDemo,
controlledDemo,
sizeDemo,
variantDemo,
rowsDemo,
widthDemo,
withFieldDemo,
} from "./demo.ts";
Expand Down Expand Up @@ -60,6 +63,24 @@ Example of TextArea in controlled mode.

<Demo data={controlledDemo} />

### Size Variants

TextArea comes in two sizes: `large` (default) and `small`.

<Demo data={sizeDemo} />

### Visual Variants

TextArea supports `default` and `borderless` visual variants.

<Demo data={variantDemo} />

### Custom Rows

TextArea defaults to 3 visible rows. Use the `rows` prop to adjust.

<Demo data={rowsDemo} />

### Custom Width

TextArea with custom width specification.
Expand All @@ -71,3 +92,4 @@ TextArea with custom width specification.
- Use with [Field](/docs/components/field) for automatic label association and error linking
- Required state is communicated via `aria-required`
- Invalid state is communicated via `aria-invalid`
- Content is scrollable when text exceeds the visible rows
18 changes: 18 additions & 0 deletions apps/www/src/content/docs/components/textarea/props.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,16 @@
export interface TextAreaProps {
/**
* Size variant of the textarea.
* @defaultValue "large"
*/
size?: 'small' | 'large';

/**
* Visual variant of the textarea.
* @defaultValue "default"
*/
variant?: 'default' | 'borderless';

/** Whether the textarea is disabled. */
disabled?: boolean;

Expand All @@ -11,6 +23,12 @@ export interface TextAreaProps {
*/
width?: string | number;

/**
* Number of visible text rows.
* @defaultValue 3
*/
rows?: number;

/** Controlled value for the textarea. */
value?: string;

Expand Down
50 changes: 47 additions & 3 deletions docs/V1-migration.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,11 @@ This guide covers all breaking changes when upgrading from the last stable Radix
- [Tabs](#tabs)
- [New Features](#new-features-8)
- [TextArea](#textarea)
- [Toast](#toast)
- [New Features](#new-features-9)
- [Tooltip](#tooltip)
- [Toast](#toast)
- [New Features](#new-features-10)
- [Tooltip](#tooltip)
- [New Features](#new-features-11)
- [New Components](#new-components)
- [Removed Exports](#removed-exports)
- [| `RadioItem` | `Radio` | See Radio |](#-radioitem--radio--see-radio-)
Expand Down Expand Up @@ -1337,6 +1338,28 @@ Key changes:

5. **`infoTooltip` prop removed** — no direct replacement. Compose manually if needed.

6. **`overflow` changed from `hidden` to `auto`.** Text that exceeds the visible area now scrolls instead of being clipped. If you relied on the old hidden-overflow behavior (e.g., pairing with JavaScript auto-resize), test that your layout still works:

```css
/* Before — content beyond the visible area was clipped */
.textarea { overflow: hidden; }

/* After — content scrolls when it exceeds visible rows */
.textarea { overflow: auto; }
```

7. **`min-height` removed; height is now row-based.** The textarea no longer has a CSS `min-height` (`var(--rs-space-13)`). Instead, the visible height is determined by the `rows` attribute (default `3`). If you depended on the old fixed minimum height, set `rows` or apply a custom `min-height` via `style` or `className`:

```tsx
// Before — min-height enforced by CSS token
<TextArea placeholder="Write something..." />

// After — height determined by rows (default 3); override if needed
<TextArea placeholder="Write something..." rows={5} />
// or restore a min-height via style
<TextArea placeholder="Write something..." style={{ minHeight: 'var(--rs-space-13)' }} />
```

**Full before/after example:**

```tsx
Expand All @@ -1357,7 +1380,27 @@ Key changes:
</Field>
```

Unchanged props: `disabled`, `placeholder`, `width`, `value`, `onChange`, and all native `<textarea>` attributes.
Unchanged props: `disabled`, `placeholder`, `width`, `value`, `onChange`, `rows`, and all native `<textarea>` attributes.

#### New Features

- `size` prop — `'large'` (default) or `'small'`. Controls padding and font size:

```tsx
<TextArea size="small" placeholder="Compact textarea" />
```

- `variant` prop — `'default'` (default) or `'borderless'`. Controls border visibility:

```tsx
<TextArea variant="borderless" placeholder="No border" />
```

- `rows` prop — sets the number of visible text rows (default `3`). Replaces the old CSS `min-height` for controlling textarea height:

```tsx
<TextArea rows={6} placeholder="Taller textarea" />
```

---

Expand Down Expand Up @@ -1610,6 +1653,7 @@ These are purely additive -- no migration needed.
- [ ] Wrap InputField usages with `<Field>` — move `label`, `helperText`, `error`, `optional` to Field props (see [InputField](#inputfield))
- [ ] Wrap TextArea usages with `<Field>` — move `label`, `helperText`, `error`, `required` to Field props (see [TextArea](#textarea))
- [ ] Remove `infoTooltip` from InputField and TextArea (no longer supported)
- [ ] Review TextArea usages for overflow behavior change (`hidden` -> `auto`) and removed `min-height` (see [TextArea](#textarea))
- [ ] Update custom CSS targeting `data-state` attributes (see [Data Attributes](#data-attributes))
- [ ] Update custom CSS referencing `--radix-*` variables (see [CSS Variables](#css-variables))
- [ ] Test all components end-to-end
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,48 @@ describe('TextArea', () => {
});
});

describe('Rows', () => {
it('defaults to 3 rows', () => {
render(<TextArea />);
const textarea = screen.getByRole('textbox');
expect(textarea).toHaveAttribute('rows', '3');
});

it('allows overriding rows', () => {
render(<TextArea rows={6} />);
const textarea = screen.getByRole('textbox');
expect(textarea).toHaveAttribute('rows', '6');
});
});

describe('Sizes', () => {
it('renders large size by default', () => {
render(<TextArea />);
const textarea = screen.getByRole('textbox');
expect(textarea).toHaveClass(styles['size-large']);
});

it('renders small size when specified', () => {
render(<TextArea size='small' />);
const textarea = screen.getByRole('textbox');
expect(textarea).toHaveClass(styles['size-small']);
});
});

describe('Variants', () => {
it('renders default variant by default', () => {
render(<TextArea />);
const textarea = screen.getByRole('textbox');
expect(textarea).toHaveClass(styles['variant-default']);
});

it('renders borderless variant when specified', () => {
render(<TextArea variant='borderless' />);
const textarea = screen.getByRole('textbox');
expect(textarea).toHaveClass(styles['variant-borderless']);
});
});

describe('Accessibility', () => {
it('supports aria-label', () => {
render(<TextArea aria-label='Message input' />);
Expand Down
34 changes: 32 additions & 2 deletions packages/raystack/components/text-area/text-area.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,14 @@
outline: none;
height: auto;
width: 100%;
min-height: var(--rs-space-13);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removing min-height might introduce unexpected height shift in the client. We should mention this in v1-migration-guide.

Example: If someone sets rows={1}, it will render a shorter textarea than before.

background-color: var(--rs-color-background-base-primary);
border: 0.5px solid var(--rs-color-border-base-tertiary);
border-radius: var(--rs-radius-2);
font-size: var(--rs-font-size-small);
line-height: var(--rs-line-height-small);
color: var(--rs-color-foreground-base-primary);
padding: var(--rs-space-3);
overflow: hidden;
overflow: auto;
}

@media (prefers-reduced-motion: no-preference) {
Expand Down Expand Up @@ -61,3 +60,34 @@
.textarea[data-invalid]:focus {
border-color: var(--rs-color-border-danger-emphasis-hover);
}

/* Size variants */
.size-large {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

.size-large is not required as its the default size and these three properties are already defined in .textarea class.

padding: var(--rs-space-3);
font-size: var(--rs-font-size-small);
line-height: var(--rs-line-height-small);
}

.size-small {
padding: var(--rs-space-2);
font-size: var(--rs-font-size-small);
line-height: var(--rs-line-height-small);
}

/* Variant styles */
.variant-default {
border: 0.5px solid var(--rs-color-border-base-tertiary);
}

.variant-borderless {
border-color: transparent;
}

.variant-borderless:hover {
border-color: transparent;
}

.variant-borderless:focus:not(:disabled) {
border-color: transparent;
outline: none;
}
Comment on lines +90 to +93
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot Apr 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Borderless variant loses visible keyboard focus state.

Line 90–93 keeps border transparent and removes outline, so focused and unfocused states become visually indistinguishable for keyboard users. Please retain a visible focus indicator for variant="borderless".

Suggested patch
 .variant-borderless:focus:not(:disabled) {
   border-color: transparent;
-  outline: none;
+  outline: 2px solid var(--rs-color-border-accent-emphasis);
+  outline-offset: 1px;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/raystack/components/text-area/text-area.module.css` around lines 90
- 93, The CSS for .variant-borderless currently removes the border and outline
on focus, making keyboard focus invisible; update the focus rule for
.variant-borderless:focus:not(:disabled) to keep a visible focus indicator
(e.g., use a non-transparent border-color, box-shadow, or outline with
accessible contrast) instead of border-color: transparent and outline: none so
keyboard users can see focus on the TextArea component (class
.variant-borderless / TextArea component).

Copy link
Copy Markdown
Contributor

@paanSinghCoder paanSinghCoder Apr 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have focus ring related improvements planned. Will be picked later.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@paanSinghCoder, understood! Would you like me to open a follow-up GitHub issue to track the focus ring improvements for the variant="borderless" textarea (and potentially other components), so it doesn't get lost?

32 changes: 29 additions & 3 deletions packages/raystack/components/text-area/text-area.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,30 @@
import { Field as FieldPrimitive } from '@base-ui/react/field';
import { cx } from 'class-variance-authority';
import { cva, cx, type VariantProps } from 'class-variance-authority';
import { ChangeEvent, type ComponentProps } from 'react';
import { useFieldContext } from '../field';

import styles from './text-area.module.css';

export interface TextAreaProps extends ComponentProps<'textarea'> {
const textAreaVariants = cva(styles.textarea, {
variants: {
size: {
small: styles['size-small'],
large: styles['size-large']
},
variant: {
default: styles['variant-default'],
borderless: styles['variant-borderless']
}
},
defaultVariants: {
size: 'large',
variant: 'default'
}
});

export interface TextAreaProps
extends Omit<ComponentProps<'textarea'>, 'size'>,
VariantProps<typeof textAreaVariants> {
disabled?: boolean;
placeholder?: string;
width?: string | number;
Expand All @@ -22,14 +41,21 @@ export function TextArea({
onChange,
placeholder,
required,
size,
variant,
...props
}: TextAreaProps) {
const fieldContext = useFieldContext();
const resolvedRequired = required ?? fieldContext?.required;

const textarea = (
<textarea
className={cx(styles.textarea, disabled && styles.disabled, className)}
rows={3}
className={cx(
textAreaVariants({ size, variant }),
disabled && styles.disabled,
className
)}
value={value}
onChange={onChange}
disabled={disabled}
Expand Down
Loading