diff --git a/apps/www/src/content/docs/components/textarea/demo.ts b/apps/www/src/content/docs/components/textarea/demo.ts
index 9d0a9f683..4a30dd098 100644
--- a/apps/www/src/content/docs/components/textarea/demo.ts
+++ b/apps/www/src/content/docs/components/textarea/demo.ts
@@ -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'
+ },
width: {
type: 'text',
defaultValue: '400px'
@@ -55,6 +65,30 @@ export const controlledDemo = {
}`
};
+export const sizeDemo = {
+ type: 'code',
+ code: `
+
+
+`
+};
+
+export const variantDemo = {
+ type: 'code',
+ code: `
+
+
+`
+};
+
+export const rowsDemo = {
+ type: 'code',
+ code: `
+
+
+`
+};
+
export const widthDemo = {
type: 'code',
code: `
+### Size Variants
+
+TextArea comes in two sizes: `large` (default) and `small`.
+
+
+
+### Visual Variants
+
+TextArea supports `default` and `borderless` visual variants.
+
+
+
+### Custom Rows
+
+TextArea defaults to 3 visible rows. Use the `rows` prop to adjust.
+
+
+
### Custom Width
TextArea with custom width specification.
@@ -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
diff --git a/apps/www/src/content/docs/components/textarea/props.ts b/apps/www/src/content/docs/components/textarea/props.ts
index 11e91a04a..c7c37a8ff 100644
--- a/apps/www/src/content/docs/components/textarea/props.ts
+++ b/apps/www/src/content/docs/components/textarea/props.ts
@@ -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;
@@ -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;
diff --git a/packages/raystack/components/text-area/__tests__/text-area.test.tsx b/packages/raystack/components/text-area/__tests__/text-area.test.tsx
index a876ffc30..99920a15f 100644
--- a/packages/raystack/components/text-area/__tests__/text-area.test.tsx
+++ b/packages/raystack/components/text-area/__tests__/text-area.test.tsx
@@ -108,6 +108,48 @@ describe('TextArea', () => {
});
});
+ describe('Rows', () => {
+ it('defaults to 3 rows', () => {
+ render();
+ const textarea = screen.getByRole('textbox');
+ expect(textarea).toHaveAttribute('rows', '3');
+ });
+
+ it('allows overriding rows', () => {
+ render();
+ const textarea = screen.getByRole('textbox');
+ expect(textarea).toHaveAttribute('rows', '6');
+ });
+ });
+
+ describe('Sizes', () => {
+ it('renders large size by default', () => {
+ render();
+ const textarea = screen.getByRole('textbox');
+ expect(textarea).toHaveClass(styles['size-large']);
+ });
+
+ it('renders small size when specified', () => {
+ render();
+ const textarea = screen.getByRole('textbox');
+ expect(textarea).toHaveClass(styles['size-small']);
+ });
+ });
+
+ describe('Variants', () => {
+ it('renders default variant by default', () => {
+ render();
+ const textarea = screen.getByRole('textbox');
+ expect(textarea).toHaveClass(styles['variant-default']);
+ });
+
+ it('renders borderless variant when specified', () => {
+ render();
+ const textarea = screen.getByRole('textbox');
+ expect(textarea).toHaveClass(styles['variant-borderless']);
+ });
+ });
+
describe('Accessibility', () => {
it('supports aria-label', () => {
render();
diff --git a/packages/raystack/components/text-area/text-area.module.css b/packages/raystack/components/text-area/text-area.module.css
index 98c102f0c..876594934 100644
--- a/packages/raystack/components/text-area/text-area.module.css
+++ b/packages/raystack/components/text-area/text-area.module.css
@@ -5,7 +5,6 @@
outline: none;
height: auto;
width: 100%;
- min-height: var(--rs-space-13);
background-color: var(--rs-color-background-base-primary);
border: 0.5px solid var(--rs-color-border-base-tertiary);
border-radius: var(--rs-radius-2);
@@ -13,7 +12,7 @@
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) {
@@ -61,3 +60,34 @@
.textarea[data-invalid]:focus {
border-color: var(--rs-color-border-danger-emphasis-hover);
}
+
+/* Size variants */
+.size-large {
+ 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;
+}
diff --git a/packages/raystack/components/text-area/text-area.tsx b/packages/raystack/components/text-area/text-area.tsx
index f5458a718..fb65bd061 100644
--- a/packages/raystack/components/text-area/text-area.tsx
+++ b/packages/raystack/components/text-area/text-area.tsx
@@ -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, 'size'>,
+ VariantProps {
disabled?: boolean;
placeholder?: string;
width?: string | number;
@@ -22,6 +41,8 @@ export function TextArea({
onChange,
placeholder,
required,
+ size,
+ variant,
...props
}: TextAreaProps) {
const fieldContext = useFieldContext();
@@ -29,7 +50,12 @@ export function TextArea({
const textarea = (