Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
d2dce7f
feat: add input package, stories, and basic input
amarques-godaddy Nov 14, 2025
2b19009
feat: add input tests and basic form example
amarques-godaddy Nov 17, 2025
1151797
feat: add input to control, add tests, update examples
amarques-godaddy Nov 20, 2025
92fe7c5
fix: remove unnecessary comments for test
amarques-godaddy Nov 20, 2025
b7364f9
fix: merge changes from main branch in
amarques-godaddy Nov 20, 2025
9ffb66a
fix: update package lock
amarques-godaddy Nov 20, 2025
5fb97bf
fix: add changeset and remove unused import from input test
amarques-godaddy Nov 20, 2025
12d76cb
fix: remove unnecessary example prop
amarques-godaddy Nov 20, 2025
5d9e2cb
fix: inline data attribute values
amarques-godaddy Nov 20, 2025
4650ee7
fix: remove duplicate render prop types and undo linter fixes
amarques-godaddy Nov 24, 2025
06c418c
fix: add full form test and remove unnecessary tests
amarques-godaddy Nov 25, 2025
2ba86ad
fix: use container and text in examples, update readme
amarques-godaddy Nov 26, 2025
ed70dc9
Merge branch 'main' into input-primitive
amarques-godaddy Nov 26, 2025
73c3a01
fix: Simplify InputProps types and readme
amarques-godaddy Dec 1, 2025
de7b503
Merge branch 'main' into input-primitive
amarques-godaddy Dec 1, 2025
5f0b454
fix: Replace native input in checkbox and radio, use forward package
amarques-godaddy Dec 1, 2025
b6dbd55
fix: simplify isTextInput to mirror React Aria implementation
amarques-godaddy Dec 2, 2025
62f1659
Merge branch 'main' into input-primitive
amarques-godaddy Dec 2, 2025
145b57b
fix: remove withForwardRef from Input
amarques-godaddy Dec 4, 2025
082df50
fix: merge main into branch
amarques-godaddy Dec 4, 2025
ee098a2
fix: add input imports back
amarques-godaddy Dec 5, 2025
3ce14f4
fix: switch labels and pre with container, remove mergeProps spread
amarques-godaddy Dec 8, 2025
5ab95aa
fix: Assign types to the args array
3rd-Eden Dec 8, 2025
9f73b60
fix: update storybook config, make file paths more specific
amarques-godaddy Dec 10, 2025
e21fcac
fix: flatten args for use in useProps, add mergedProps, ref still not…
amarques-godaddy Dec 12, 2025
f9f6e5b
fix: update useProps to latest to use ref from destructured object
amarques-godaddy Dec 12, 2025
8230052
fix: correct package references
3rd-Eden Dec 12, 2025
33e0433
fix: use props.ref instead of destructured ref
amarques-godaddy Dec 12, 2025
7c1089e
fix: add noops and remove story alert to reduce test warnings
amarques-godaddy Dec 12, 2025
59200fa
fix: remake changeset, remove as any from checkbox ref
amarques-godaddy Dec 15, 2025
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
10 changes: 10 additions & 0 deletions .changeset/hot-hornets-poke.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
"@bento/input": minor
"@bento/scroll-lock": patch
"@bento/focus-lock": patch
"@bento/container": patch
"@bento/dismiss": patch
"@bento/portal": patch
---

Adds @bento/input to the repo and removes unused dependencies.
3 changes: 3 additions & 0 deletions apps/docs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,17 @@
"@bento/environment": "*",
"@bento/error": "*",
"@bento/field-error": "*",
"@bento/focus-lock": "*",
"@bento/heading": "*",
"@bento/icon": "*",
"@bento/illustration": "*",
"@bento/input": "*",
"@bento/internal-props": "*",
"@bento/listbox": "*",
"@bento/portal": "*",
"@bento/pressable": "*",
"@bento/radio": "*",
"@bento/scroll-lock": "*",
"@bento/slots": "*",
"@bento/storybook-addon-helpers": "*",
"@bento/svg-parser": "*",
Expand Down
1,888 changes: 1,172 additions & 716 deletions package-lock.json

Large diffs are not rendered by default.

1 change: 0 additions & 1 deletion packages/container/examples/empty.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,3 @@ export function EmptyExample(args: ContainerProps) {
</Container>
);
}

2 changes: 1 addition & 1 deletion packages/container/test/container.browser.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ describe('@bento/container', function bento() {
// Parent container passes slots to nested child
const { container } = render(
<Container slots={{ inner: { as: 'section' } }} data-testid="outer">
{ /* We assign as="article" to verify that slot overrides are correctly applied */ }
{/* We assign as="article" to verify that slot overrides are correctly applied */}
<Container slot="inner" as="article" data-testid="inner">
Inner content
</Container>
Expand Down
1 change: 0 additions & 1 deletion packages/dismiss/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,3 @@
},
"exclude": ["test", "examples"]
}

1 change: 0 additions & 1 deletion packages/dismiss/tsup.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,3 @@ export default defineConfig({
...shared,
entry: ['src/index.tsx']
});

Comment thread
amarques-godaddy marked this conversation as resolved.
Outdated
1 change: 0 additions & 1 deletion packages/dismiss/vitest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,3 @@ export default mergeConfig(
}
})
);

1 change: 0 additions & 1 deletion packages/focus-lock/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,6 @@
"@bento/container": "*",
"@bento/heading": "*",
"@bento/listbox": "*",
"@bento/portal": "*",
"@bento/radio": "*",
"@bento/text": "*"
},
Expand Down
9 changes: 9 additions & 0 deletions packages/input/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
The MIT License (MIT)

Copyright (c) 2025 GoDaddy Operating Company, LLC.

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
171 changes: 171 additions & 0 deletions packages/input/README.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
import {
Meta,
Story,
ArgTypes,
Controls,
Source,
} from '@storybook/addon-docs/blocks';
import * as Stories from './input.stories.tsx';

import SourceControlled from './examples/controlled.tsx?raw';
import SourceForm from './examples/basic-form.tsx?raw';
import SourceUncontrolled from './examples/uncontrolled.tsx?raw';

<Meta of={Stories} name="Overview" />

# Input

The `@bento/input` package provides a universal input primitive component that renders an `<input>` element with React Aria interactions. It supports all HTML input types with proper accessibility, hover, and focus management.

This primitive is built with accessibility and flexibility in mind, providing state-based render props, comprehensive data attributes, and integration with React Aria's focus and hover hooks. It can be used for text inputs, checkboxes, radio buttons, file uploads, and any other input type supported by HTML.

## Installation

```shell
npm install --save @bento/input
```

## Props

The `@bento/input` package exports the `Input` component:

```jsx
import { Input } from '@bento/input';

<Input type="text" placeholder="Enter text" />
```

The following properties are available to be used on the `Input` component:

<ArgTypes of={Stories.Props} />

For all other properties specified on the `Input` component, the component
will pass them down to the underlying `<input>` element. This includes properties
such as `id`, `data-*` attributes, or additional ARIA attributes that you might
need for specialized use cases.

## Examples

### Controlled Input

The most common pattern for the `Input` component is to use it as a controlled component, where the value is managed by React state. This allows you to easily read and update the input value.

<Source language='tsx' code={SourceControlled} />
<Story of={Stories.Controlled} inline />
<Controls of={Stories.Controlled} />

In this example, the input value is stored in state and updated via the `onChange` handler. This is the recommended pattern for most use cases where you need to read or validate the input value.

### Uncontrolled Input

For simpler use cases where you don't need to track the input value in React state, you can use an uncontrolled input with a `defaultValue`. The DOM will manage the input's value internally.

<Source language='tsx' code={SourceUncontrolled} />
<Story of={Stories.Uncontrolled} inline />
<Controls of={Stories.Uncontrolled} />

Uncontrolled inputs are useful when you only need to read the value on form submission, or when integrating with form libraries that manage values through refs.

### Form Example

The `Input` component supports all HTML input types, making it versatile for building complete forms. This example demonstrates multiple input types working together in a single form.

<Source language='tsx' code={SourceForm} />
<Story of={Stories.BasicForm} inline collapsed />
<Controls of={Stories.BasicForm} />

The component intelligently handles type-specific props, ensuring that only relevant attributes are applied to each input type. For example, `min` and `max` are only applied to number and range inputs, while `checked` is only applied to checkbox and radio inputs.

## Usage Guidelines

The `Input` component is a low-level primitive that provides the foundation for building input fields. Understanding when and how to use it will help you create accessible, user-friendly forms.

### Supported Input Types

The `Input` component supports all HTML input types:

**Text-based inputs:** `text`, `email`, `password`, `url`, `tel`, `search`
**Numeric inputs:** `number`, `range`
**Date/time inputs:** `date`, `datetime-local`, `time`, `month`, `week`
**Choice inputs:** `checkbox`, `radio`
**File input:** `file`
**Color input:** `color`
**Hidden input:** `hidden`

Each input type receives appropriate type-specific props automatically. You don't need to worry about which props are valid for which types—the component handles this for you.

### Accessibility Best Practices

Always provide labels for your inputs using the `<label>` element with a matching `htmlFor` attribute, or by using `aria-label` or `aria-labelledby`. Never rely solely on `placeholder` text for labeling, as it disappears when the user starts typing.

For invalid inputs, set `aria-invalid` to provide semantic meaning to assistive technologies. Combine this with `aria-describedby` to reference error messages that explain what went wrong.

Use appropriate input types to enable better mobile keyboards and native browser features. For example, `type="email"` shows an email-optimized keyboard on mobile devices, while `type="date"` shows a native date picker.

## Accessibility

The `Input` component is built with accessibility as a core requirement. It provides comprehensive keyboard support, ARIA attributes, and integration with React Aria's focus management system.

**Keyboard Navigation**

All input types are fully keyboard accessible. Text inputs can be focused with Tab and navigated with arrow keys. Checkboxes and radio buttons can be toggled with Space. The component integrates with React Aria's `useFocusRing` hook to provide intelligent focus indicators that only appear during keyboard navigation, not mouse clicks.

**Focus Management**

The component distinguishes between mouse focus and keyboard focus through the `isFocused` and `isFocusVisible` states. This allows you to style focus rings appropriately—showing them only when the user is navigating with a keyboard, which reduces visual noise for mouse users while maintaining accessibility for keyboard users.

**ARIA Support**

The component passes through all ARIA attributes you provide, including `aria-label`, `aria-labelledby`, `aria-describedby`, and `aria-invalid`. These attributes are essential for screen reader users to understand the purpose and state of form fields.

**State Communication**

The component provides comprehensive data attributes that communicate its state to both CSS selectors and assistive technologies. These include `data-focused`, `data-focus-visible`, `data-hovered`, `data-disabled`, `data-invalid`, `data-readonly`, `data-required`, `data-empty`, and `data-checked`. These attributes make it easy to style inputs based on their state and ensure consistent visual feedback.
Comment thread
amarques-godaddy marked this conversation as resolved.
Outdated

## Customization

The `Input` component is built using the `@bento/slots` package, allowing you to customize styling based on component state through render props and data attributes.

### Render Props

The `className` and `style` props can accept functions that receive render props describing the current state of the input:
Comment thread
amarques-godaddy marked this conversation as resolved.
Outdated

```jsx
<Input
type="text"
className={(state) =>
state.isFocusVisible ? 'input-focused' : 'input-default'
}
style={(state) => ({
borderColor: state.isInvalid ? 'red' : state.isFocused ? 'blue' : 'gray'
})}
/>
```

The render props object includes:
Comment thread
amarques-godaddy marked this conversation as resolved.
Outdated
- `isHovered`: Whether the input is currently hovered with a mouse
- `isFocused`: Whether the input is focused (via mouse or keyboard)
- `isFocusVisible`: Whether the input is keyboard focused
- `isDisabled`: Whether the input is disabled
- `isInvalid`: Whether the input has `aria-invalid` set

### Data Attributes

The component automatically applies data attributes that correspond to its state, allowing you to style with CSS selectors:

| Attribute | Description | Example Values |
| ---------------------- | ---------------------------------------------- | ----------------- |
| `data-focused` | Whether the input is focused | "true" / "false" |
| `data-focus-visible` | Whether keyboard focus ring should be visible | "true" / "false" |
| `data-hovered` | Whether the input is hovered | "true" / "false" |
| `data-disabled` | Whether the input is disabled | "true" / "false" |
| `data-invalid` | Whether the input is invalid | "true" / "false" |
| `data-readonly` | Whether the input is read-only | "true" / "false" |
| `data-required` | Whether the input is required | "true" / "false" |
| `data-empty` | Whether the input has no value | "true" / "false" |
| `data-checked` | Whether checkbox/radio is checked | "true" / "false" |

### Slots

The component is registered as `BentoInput` in the slots system. While the base Input component doesn't introduce additional slots, it can be extended with slot-based composition for building higher-level components.

61 changes: 61 additions & 0 deletions packages/input/examples/basic-form.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/* v8 ignore next */
import React, { useState } from 'react';
import { Input } from '../src/index.tsx';

export function BasicFormExample() {
const [formData, setFormData] = useState({
name: '',
email: ''
});

function handleChange(field: string) {
function onInputChange(e: React.ChangeEvent<HTMLInputElement>) {
setFormData((prev) => ({ ...prev, [field]: e.target.value }));
}
return onInputChange;
}

return (
<form>
<h2>Basic Form</h2>

<div>
<label htmlFor="name">Name</label>
Comment thread
amarques-godaddy marked this conversation as resolved.
Outdated
<Input
id="name"
type="text"
value={formData.name}
onChange={handleChange('name')}
placeholder="Enter your name"
/>
</div>

<div>
<label htmlFor="email">Email</label>
<Input
id="email"
type="email"
value={formData.email}
onChange={handleChange('email')}
placeholder="you@example.com"
required
/>
</div>

<div>
<Input
type="submit"
onClick={function HandleClick(e) {
e.preventDefault();
}}
value="Submit"
/>
</div>

<div>
<h3>Form Data (live preview):</h3>
<pre>{JSON.stringify(formData, null, 2)}</pre>
</div>
</form>
);
}
16 changes: 16 additions & 0 deletions packages/input/examples/controlled.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/* v8 ignore next */
import React, { useState } from 'react';
import { Input } from '@bento/input';

export function ControlledInput() {
const [value, setValue] = useState('');
return (
<Input
value={value}
onChange={function ChangeEvent(e) {
setValue(e.target.value);
}}
type="text"
/>
);
}
Loading