# Input

Displays a form input field for a single line of text.

```tsx
import * as stylex from "@stylexjs/stylex"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { VStack } from "@/components/ui/stack"
import { color, leading, text } from "@/theme/tokens.stylex"

const styles = stylex.create({
  field: { width: "100%", maxWidth: "20rem" },
  hint: { color: color.mutedForeground, fontSize: text.sm, lineHeight: leading.sm },
})

export default function InputDemo() {
  return (
    <VStack gap={2} xstyle={styles.field}>
      <Label htmlFor="input-demo-key">API key</Label>
      <Input id="input-demo-key" type="password" placeholder="sk-..." />
      <span {...stylex.props(styles.hint)}>Encrypted at rest. Rotate it any time.</span>
    </VStack>
  )
}
```

## Installation

1. Install the following dependencies:

   ```bash
   npm install @base-ui/react @stylexjs/stylex
   ```

2. Set up the token layer and the StyleX compiler, if you have not already. See
   [Installation](https://carbon.style/docs/installation), and paste `states.ts` from the same page.
   The input's field surface, its focus ring and its invalid ring come from it.

   Paste `field-state.ts` and `state-styles.ts` from the same installation page.

3. Copy and paste the following code into your project.

   ```tsx title="components/ui/input.tsx"
   "use client"

   import { createContext, use } from "react"
   import { Input as InputPrimitive } from "@base-ui/react/input"
   import * as stylex from "@stylexjs/stylex"
   import type { StyleXStyles } from "@stylexjs/stylex"
   import type { CSSProperties } from "react"
   import { stateStyles } from "@/lib/state-styles"

   import { breakpoints } from "@/theme/breakpoints.stylex"
   import { FieldStateContext } from "@/lib/field-state"
   import { states } from "@/theme/states"
   import { color, duration, ease, leading, radius, space, text } from "@/theme/tokens.stylex"

   const InputGroupContext = createContext<{ disabled: boolean; invalid: boolean } | null>(null)

   const styles = stylex.create({
     base: {
       width: "100%",
       minWidth: 0,
       height: space.s8,
       borderRadius: radius.lg,
       borderWidth: 1,
       borderStyle: "solid",
       paddingInline: space.s2_5,
       paddingBlock: space.s1,
       fontSize: { default: text.base, [breakpoints.md]: text.sm },
       lineHeight: { default: leading.base, [breakpoints.md]: leading.sm },
       color: color.foreground,
       transitionProperty: "color, background-color, border-color, box-shadow",
       transitionDuration: duration.color,
       transitionTimingFunction: ease.standard,
       opacity: { default: 1, ":disabled": 0.5 },
       pointerEvents: { default: null, ":disabled": "none" },
       "::placeholder": { color: color.mutedForeground },
       "::file-selector-button": {
         height: space.s6,
         marginInlineEnd: space.s2,
         borderWidth: 0,
         backgroundColor: color.transparent,
         color: color.foreground,
         fontSize: text.sm,
         fontWeight: 500,
       },
     },
     inGroup: {
       flexGrow: 1,
       flexBasis: 0,
       borderWidth: 0,
       borderRadius: 0,
       backgroundColor: { default: color.transparent, ":disabled": color.transparent },
       boxShadow: { default: "none", ":focus-visible": "none" },
       opacity: { default: 1, ":disabled": 1 },
     },
   })

   const inputStyles = {
     base: [styles.base, states.focusRing, states.fieldSurface],
     invalid: states.invalidRing,
     inGroup: styles.inGroup,
   }

   type InputProps = Omit<InputPrimitive.Props, "className" | "style"> & {
     style?: CSSProperties
     xstyle?: StyleXStyles
     className?: string
   }

   function Input({
     disabled,
     "aria-invalid": ariaInvalid,
     xstyle,
     className,
     style,
     ...props
   }: InputProps) {
     const fieldState = use(FieldStateContext)
     const group = use(InputGroupContext)
     const invalid = group?.invalid || (ariaInvalid != null && ariaInvalid !== false)

     return (
       <InputPrimitive
         data-slot={group ? "input-group-control" : "input"}
         {...props}
         disabled={group?.disabled || disabled}
         aria-invalid={invalid || undefined}
         {...stateStyles(
           (state) => [
             styles.base,
             states.focusRing,
             states.fieldSurface,
             (state.valid === false || fieldState?.valid === false || invalid) &&
               !group &&
               states.invalidRing,
             state.disabled && !group && states.disabled,
             group && styles.inGroup,
             xstyle,
           ],
           { className, style },
         )}
       />
     )
   }

   export { Input, InputGroupContext, inputStyles }
   ```

4. Update the import paths to match your project setup.

## Usage

```tsx
import { Input } from "@/components/ui/input"
```

```tsx
<Input type="email" placeholder="me@example.com" />
```

## Basic

```tsx
import * as stylex from "@stylexjs/stylex"
import { Input } from "@/components/ui/input"

const styles = stylex.create({
  field: { width: "100%", maxWidth: "20rem" },
})

export default function InputBasic() {
  return <Input placeholder="Enter text" xstyle={styles.field} />
}
```

## Label

Pair the input with a [Label](https://carbon.style/docs/components/label) and match `htmlFor` to
the input's `id`. A [Field](https://carbon.style/docs/components/field) adds the description and
error text around it.

```tsx
import * as stylex from "@stylexjs/stylex"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { VStack } from "@/components/ui/stack"

const styles = stylex.create({
  field: { width: "100%", maxWidth: "20rem" },
})

export default function LabelDemo() {
  return (
    <VStack gap={2} xstyle={styles.field}>
      <Label htmlFor="label-demo-email">Email</Label>
      <Input id="label-demo-email" type="email" placeholder="me@example.com" />
    </VStack>
  )
}
```

## Disabled

Set `disabled` on `Field` to disable its control and dim its label, or on a bare control directly.

```tsx
import { Field, FieldLabel } from "@/components/ui/field"
import * as stylex from "@stylexjs/stylex"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { VStack } from "@/components/ui/stack"

const styles = stylex.create({
  field: { width: "100%", maxWidth: "20rem" },
})

function BareDisabled() {
  return (
    <VStack gap={2} xstyle={styles.field}>
      <Label htmlFor="input-disabled-email" disabled>
        Email
      </Label>
      <Input id="input-disabled-email" type="email" placeholder="me@example.com" disabled />
    </VStack>
  )
}

export default function DisabledExample() {
  return (
    <VStack gap={5}>
      <BareDisabled />
      <Field disabled>
        <FieldLabel>Email</FieldLabel>
        <Input type="email" placeholder="me@example.com" />
      </Field>
    </VStack>
  )
}
```

## Invalid

Set `invalid` on `Field` to style its control, or `aria-invalid` on a bare control. The examples show both.

```tsx
import { Input } from "@/components/ui/input"
import { Field, FieldLabel, FieldError } from "@/components/ui/field"
import { Label } from "@/components/ui/label"
import { VStack } from "@/components/ui/stack"

export default function InputInvalid() {
  return (
    <VStack gap={5} width="100%" maxWidth="24rem">
      <VStack gap={2}>
        <Label htmlFor="input-invalid-bare">Email</Label>
        <Input
          id="input-invalid-bare"
          defaultValue="not-an-email"
          aria-invalid
          aria-describedby="input-invalid-bare-error"
        />
        <FieldError id="input-invalid-bare-error">Enter a valid email address.</FieldError>
      </VStack>
      <Field invalid>
        <FieldLabel>Email</FieldLabel>
        <Input defaultValue="not-an-email" />
        <FieldError>Enter a valid email address.</FieldError>
      </Field>
    </VStack>
  )
}
```

## File

Use `type="file"` for a file input. The browser's own button is restyled to the
control's type scale so the height does not change.

```tsx
import * as stylex from "@stylexjs/stylex"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { VStack } from "@/components/ui/stack"

const styles = stylex.create({
  field: { width: "100%", maxWidth: "20rem" },
})

export default function InputFile() {
  return (
    <VStack gap={2} xstyle={styles.field}>
      <Label htmlFor="input-file-avatar">Avatar</Label>
      <Input id="input-file-avatar" type="file" accept="image/*" />
    </VStack>
  )
}
```

## Required

Use the `required` attribute for required inputs. Mark the label too, since
`required` alone is only announced once the control has focus.

```tsx
import * as stylex from "@stylexjs/stylex"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { VStack } from "@/components/ui/stack"
import { color } from "@/theme/tokens.stylex"

const styles = stylex.create({
  field: { width: "100%", maxWidth: "20rem" },
  marker: { color: color.destructive },
})

export default function InputRequired() {
  return (
    <VStack gap={2} xstyle={styles.field}>
      <Label htmlFor="input-required-name">
        Workspace name
        <span {...stylex.props(styles.marker)} aria-hidden>
          *
        </span>
      </Label>
      <Input id="input-required-name" placeholder="Acme" required />
    </VStack>
  )
}
```

## Inline

Put the input and a button in an `HStack` for a one-line form.

```tsx
import * as stylex from "@stylexjs/stylex"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { HStack } from "@/components/ui/stack"

const styles = stylex.create({
  row: { width: "100%", maxWidth: "20rem" },
})

export default function InputInline() {
  return (
    <HStack gap={2} xstyle={styles.row}>
      <Input type="email" placeholder="me@example.com" aria-label="Email" />
      <Button variant="outline">Subscribe</Button>
    </HStack>
  )
}
```

## Grid

Lay several inputs out in a grid to place them side by side.

```tsx
import * as stylex from "@stylexjs/stylex"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { VStack } from "@/components/ui/stack"
import { space } from "@/theme/tokens.stylex"

const styles = stylex.create({
  grid: {
    display: "grid",
    gridTemplateColumns: "1fr 1fr",
    gap: space.s4,
    width: "100%",
    maxWidth: "26rem",
  },
})

export default function InputGrid() {
  return (
    <div {...stylex.props(styles.grid)}>
      <VStack gap={2}>
        <Label htmlFor="input-grid-first">First name</Label>
        <Input id="input-grid-first" placeholder="Ada" />
      </VStack>
      <VStack gap={2}>
        <Label htmlFor="input-grid-last">Last name</Label>
        <Input id="input-grid-last" placeholder="Lovelace" />
      </VStack>
    </div>
  )
}
```

## Input group

To put an icon, a prefix or a button inside the field, wrap it in
[InputGroup](https://carbon.style/docs/components/input-group). The input detects the group and
drops its own border, fill and ring so the group can draw them once.

```tsx
import * as stylex from "@stylexjs/stylex"
import { InputGroup, InputGroupAddon, InputGroupText } from "@/components/ui/input-group"
import { Input } from "@/components/ui/input"
import { Search } from "lucide-react"

const styles = stylex.create({
  group: { maxWidth: "20rem" },
})

export default function InputInputGroup() {
  return (
    <InputGroup xstyle={styles.group}>
      <Input placeholder="Search docs" aria-label="Search docs" />
      <InputGroupAddon>
        <Search />
      </InputGroupAddon>
      <InputGroupAddon align="inline-end">
        <InputGroupText>⌘K</InputGroupText>
      </InputGroupAddon>
    </InputGroup>
  )
}
```

## Button group

To attach a button to the outside of the field, use
[ButtonGroup](https://carbon.style/docs/components/button-group). Square the input's trailing
corners yourself. The group's joining rules apply to `Button`, which reads the
group through context, and `Input` does not.

```tsx
import * as stylex from "@stylexjs/stylex"
import { Button } from "@/components/ui/button"
import { ButtonGroup } from "@/components/ui/button-group"
import { Input } from "@/components/ui/input"
import { Copy } from "lucide-react"

const styles = stylex.create({
  group: { width: "100%", maxWidth: "20rem" },
  input: {
    flexGrow: 1,
    minWidth: 0,
    borderStartEndRadius: 0,
    borderEndEndRadius: 0,
    borderInlineEndWidth: 0,
  },
})

export default function InputButtonGroup() {
  return (
    <ButtonGroup xstyle={styles.group} aria-label="Invite link">
      <Input
        readOnly
        defaultValue="https://carbon.style/invite/8fa2"
        aria-label="Invite link"
        xstyle={styles.input}
      />
      <Button variant="outline" size="icon" aria-label="Copy link">
        <Copy />
      </Button>
    </ButtonGroup>
  )
}
```

> **Note**
>
> Below the `md` breakpoint the font size is 16px rather than 14px. iOS Safari
> zooms the page when a focused control's text is any smaller, and the zoom does
> not reverse on blur.

## API reference

Pass `render` to replace a part's element or compose it with another component.

`Input` wraps the Base UI `Input` part and accepts its props, including
`defaultValue`, `value` and `onValueChange`. See the
[Base UI reference](https://base-ui.com/react/components/input).

### Input

| Prop            | Type                                        | Default |
| --------------- | ------------------------------------------- | ------- |
| `type`          | every `<input>` type                        | `text`  |
| `disabled`      | `boolean`                                   | `false` |
| `aria-invalid`  | `boolean`                                   | —       |
| `onValueChange` | `(value: string, details) => void`          | —       |
| `render`        | `ReactElement \| (props, state) => Element` | —       |
| `xstyle`        | `StyleXStyles`                              | —       |
| `className`     | `string`                                    | —       |

Inside an `InputGroup` the group's `disabled` and `invalid` win. An input can be
disabled on its own, but it cannot opt out of a disabled group.

### inputStyles

The style maps behind the component. Spread them onto any element.

| Key       | What it is                                                  |
| --------- | ----------------------------------------------------------- |
| `base`    | The control: frame, fill, type scale, placeholder, disabled |
| `invalid` | The destructive border and ring                             |
| `inGroup` | The frameless form an `InputGroup` member takes             |

`base` is three styles, in merge order: the control's own, the shared
`states.focusRing`, and `states.fieldSurface`, which is the hairline edge, the
fill and the drop. `invalid` is `states.invalidRing` from `theme/states.ts`. Set
a border of your own through `--carbon-border-color` rather than `borderColor`.
A flat `borderColor` replaces the property with all of its conditions, so the ring loses its focus colour.

### Data attributes

| Attribute                                   | On                  | Effect                                     |
| ------------------------------------------- | ------------------- | ------------------------------------------ |
| `data-slot="input"`                         | a standalone input  | Targets the control from a parent          |
| `data-slot="input-group-control"`           | an input in a group | What `InputGroup` watches to draw its ring |
| `data-disabled`, `data-valid`, `data-dirty` | the control         | Base UI writes them inside a `Field`       |
