diff --git a/apps/www/src/content/docs/components/input/demo.ts b/apps/www/src/content/docs/components/input/demo.ts
index 25778c69c..ecf0f8bfc 100644
--- a/apps/www/src/content/docs/components/input/demo.ts
+++ b/apps/www/src/content/docs/components/input/demo.ts
@@ -129,6 +129,24 @@ export const sizeChipDemo = {
`
};
+export const onValueChangeDemo = {
+ type: 'code',
+ code: `function ValueChangeExample() {
+ const [value, setValue] = React.useState("");
+
+ return (
+
+
+ Current value: {value || "(empty)"}
+
+ );
+}`
+};
+
export const interactiveChipDemo = {
type: 'code',
style: {
diff --git a/apps/www/src/content/docs/components/input/index.mdx b/apps/www/src/content/docs/components/input/index.mdx
index 5f17e2454..756c8fca1 100644
--- a/apps/www/src/content/docs/components/input/index.mdx
+++ b/apps/www/src/content/docs/components/input/index.mdx
@@ -16,6 +16,7 @@ import {
sizeChipDemo,
interactiveChipDemo,
withFieldDemo,
+ onValueChangeDemo,
} from "./demo.ts";
@@ -60,6 +61,12 @@ Use Field to add label, description, and error handling.
+### Controlled Value
+
+Use `onValueChange` for a callback that receives only the new string value, or `onChange` for the full React change event. Both fire on every keystroke — pick whichever fits your needs.
+
+
+
### With Prefix/Suffix
Input with prefix and suffix text.
diff --git a/apps/www/src/content/docs/components/input/props.ts b/apps/www/src/content/docs/components/input/props.ts
index 01dab7581..d8e337a4a 100644
--- a/apps/www/src/content/docs/components/input/props.ts
+++ b/apps/www/src/content/docs/components/input/props.ts
@@ -43,6 +43,18 @@ export interface InputProps {
/** Ref to the outer container div. */
containerRef?: React.RefObject;
+ /** The controlled value of the input. */
+ value?: string;
+
+ /** Native change handler. Receives the React change event. */
+ onChange?: (event: React.ChangeEvent) => void;
+
+ /**
+ * Convenience callback fired with the new string value.
+ * Provided by Base UI's Input primitive — use this when you only need the value.
+ */
+ onValueChange?: (value: string, eventDetails: unknown) => void;
+
/** Additional CSS class names. */
className?: string;
}
diff --git a/apps/www/src/content/docs/components/search/demo.ts b/apps/www/src/content/docs/components/search/demo.ts
index 26eab6174..c35e4b0c9 100644
--- a/apps/www/src/content/docs/components/search/demo.ts
+++ b/apps/www/src/content/docs/components/search/demo.ts
@@ -39,3 +39,23 @@ export const clearDemo = {
`
};
+
+export const onValueChangeDemo = {
+ type: 'code',
+ code: `function SearchValueChangeExample() {
+ const [query, setQuery] = React.useState("");
+
+ return (
+
+ setQuery("")}
+ />
+ Query: {query || "(empty)"}
+
+ );
+}`
+};
diff --git a/apps/www/src/content/docs/components/search/index.mdx b/apps/www/src/content/docs/components/search/index.mdx
index 0ecf20aca..b5f9606ce 100644
--- a/apps/www/src/content/docs/components/search/index.mdx
+++ b/apps/www/src/content/docs/components/search/index.mdx
@@ -4,7 +4,7 @@ description: A search input component with built-in search icon and optional cle
source: packages/raystack/components/search
---
-import { playground, sizeDemo, clearDemo } from "./demo.ts";
+import { playground, sizeDemo, clearDemo, onValueChangeDemo } from "./demo.ts";
@@ -38,6 +38,12 @@ The Search component can include a clear button that appears when there is input
+### Controlled Value
+
+Use `onValueChange` to receive only the new query string, or `onChange` for the full React change event. The Search component forwards both to the underlying [Input](/docs/components/input).
+
+
+
## Accessibility
The Search component is built with accessibility in mind, following ARIA best practices:
diff --git a/apps/www/src/content/docs/components/search/props.ts b/apps/www/src/content/docs/components/search/props.ts
index 3f6d212fe..8d04db533 100644
--- a/apps/www/src/content/docs/components/search/props.ts
+++ b/apps/www/src/content/docs/components/search/props.ts
@@ -17,8 +17,14 @@ export interface SearchProps {
/** The controlled value of the input. */
value?: string;
- /** Callback when input value changes. */
- onChange?: (value: string) => void;
+ /** Native change handler. Receives the React change event. */
+ onChange?: (event: React.ChangeEvent) => void;
+
+ /**
+ * Convenience callback fired with the new string value.
+ * Forwarded to the underlying Input — use this when you only need the value.
+ */
+ onValueChange?: (value: string, eventDetails: unknown) => void;
/** Callback when clear button is clicked. */
onClear?: () => void;
diff --git a/apps/www/src/content/docs/components/textarea/demo.ts b/apps/www/src/content/docs/components/textarea/demo.ts
index c4112175f..c4218c22f 100644
--- a/apps/www/src/content/docs/components/textarea/demo.ts
+++ b/apps/www/src/content/docs/components/textarea/demo.ts
@@ -71,6 +71,23 @@ export const controlledDemo = {
}`
};
+export const onValueChangeDemo = {
+ type: 'code',
+ code: `function TextAreaValueChangeExample() {
+ const [value, setValue] = React.useState('');
+
+ return (
+
+
+
+ );
+}`
+};
+
export const sizeDemo = {
type: 'code',
code: `
diff --git a/apps/www/src/content/docs/components/textarea/index.mdx b/apps/www/src/content/docs/components/textarea/index.mdx
index d90622f57..81f8b0f01 100644
--- a/apps/www/src/content/docs/components/textarea/index.mdx
+++ b/apps/www/src/content/docs/components/textarea/index.mdx
@@ -8,6 +8,7 @@ import {
playground,
basicDemo,
controlledDemo,
+ onValueChangeDemo,
sizeDemo,
variantDemo,
rowsDemo,
@@ -63,6 +64,12 @@ Example of TextArea in controlled mode.
+### Using `onValueChange`
+
+`onValueChange` is a convenience callback that fires alongside `onChange` and receives just the new string value. Use it when you don't need the full React change event.
+
+
+
### Size Variants
TextArea comes in two sizes: `large` (default) and `small`.
diff --git a/apps/www/src/content/docs/components/textarea/props.ts b/apps/www/src/content/docs/components/textarea/props.ts
index c7c37a8ff..8016591f8 100644
--- a/apps/www/src/content/docs/components/textarea/props.ts
+++ b/apps/www/src/content/docs/components/textarea/props.ts
@@ -32,9 +32,18 @@ export interface TextAreaProps {
/** Controlled value for the textarea. */
value?: string;
- /** Change handler for controlled usage. */
+ /** Change handler for controlled usage. Receives the React change event. */
onChange?: (event: React.ChangeEvent) => void;
+ /**
+ * Convenience callback fired alongside `onChange` with the new string value.
+ * Use this when you only need the value rather than the full event.
+ */
+ onValueChange?: (
+ value: string,
+ event: React.ChangeEvent
+ ) => void;
+
/** Additional CSS class names. */
className?: string;
}
diff --git a/docs/V1-migration.md b/docs/V1-migration.md
index cd4dab891..b9d8ba14c 100644
--- a/docs/V1-migration.md
+++ b/docs/V1-migration.md
@@ -1168,6 +1168,16 @@ Unchanged props: `size`, `variant`, `disabled`, `leadingIcon`, `trailingIcon`, `
- `DatePicker.inputFieldProps` → `DatePicker.inputProps`
- `RangePicker.inputFieldsProps` → `RangePicker.inputsProps`
+#### New Features
+
+- `onValueChange` callback — provided by Base UI's Input primitive. Fires with the new string value (and an `eventDetails` second arg) alongside the standard `onChange`. Use it when you only need the value:
+
+```tsx
+
+```
+
+The `Search` component forwards `onValueChange` to the underlying Input, so it works there as well.
+
---
### Label
@@ -1827,6 +1837,12 @@ Unchanged props: `disabled`, `placeholder`, `width`, `value`, `onChange`, `rows`
```
+- `onValueChange` callback — fires alongside `onChange` with the new string value (and the React change event as the second arg). Use it when you only need the value:
+
+```tsx
+
+```
+
---
### Toast
diff --git a/packages/raystack/components/search/search.tsx b/packages/raystack/components/search/search.tsx
index 23a3ed5b2..63335eeab 100644
--- a/packages/raystack/components/search/search.tsx
+++ b/packages/raystack/components/search/search.tsx
@@ -14,14 +14,12 @@ export interface SearchProps extends Omit {
}
export function Search({
- className,
disabled,
placeholder = 'Search',
size,
showClearButton,
onClear,
value,
- onChange,
width = '100%',
variant = 'default',
...props
@@ -54,9 +52,7 @@ export function Search({
placeholder={placeholder}
disabled={disabled}
value={value}
- onChange={onChange}
size={size}
- className={className}
aria-label={placeholder}
variant={variant}
{...props}
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 99920a15f..89f3062c7 100644
--- a/packages/raystack/components/text-area/__tests__/text-area.test.tsx
+++ b/packages/raystack/components/text-area/__tests__/text-area.test.tsx
@@ -71,6 +71,30 @@ describe('TextArea', () => {
});
describe('Event Handling', () => {
+ it('calls onValueChange with the new string value', () => {
+ const handleValueChange = vi.fn();
+ render();
+ const textarea = screen.getByRole('textbox');
+
+ fireEvent.change(textarea, { target: { value: 'hello' } });
+ expect(handleValueChange).toHaveBeenCalledTimes(1);
+ expect(handleValueChange.mock.calls[0][0]).toBe('hello');
+ expect(handleValueChange.mock.calls[0][1]).toBeDefined();
+ });
+
+ it('calls both onChange and onValueChange', () => {
+ const handleChange = vi.fn();
+ const handleValueChange = vi.fn();
+ render(
+
+ );
+ const textarea = screen.getByRole('textbox');
+
+ fireEvent.change(textarea, { target: { value: 'hi' } });
+ expect(handleChange).toHaveBeenCalledTimes(1);
+ expect(handleValueChange).toHaveBeenCalledTimes(1);
+ });
+
it('handles onFocus event', () => {
const handleFocus = vi.fn();
render();
diff --git a/packages/raystack/components/text-area/text-area.tsx b/packages/raystack/components/text-area/text-area.tsx
index fb65bd061..04c8e4f0b 100644
--- a/packages/raystack/components/text-area/text-area.tsx
+++ b/packages/raystack/components/text-area/text-area.tsx
@@ -30,6 +30,10 @@ export interface TextAreaProps
width?: string | number;
value?: string;
onChange?: (event: ChangeEvent) => void;
+ onValueChange?: (
+ value: string,
+ event: ChangeEvent
+ ) => void;
}
export function TextArea({
@@ -39,6 +43,7 @@ export function TextArea({
width = '100%',
value,
onChange,
+ onValueChange,
placeholder,
required,
size,
@@ -48,6 +53,11 @@ export function TextArea({
const fieldContext = useFieldContext();
const resolvedRequired = required ?? fieldContext?.required;
+ const handleChange = (event: ChangeEvent) => {
+ onChange?.(event);
+ onValueChange?.(event.target.value, event);
+ };
+
const textarea = (