` |
+| `marginPlacement` | `'above' \| 'below'` | — | Adds a `field-description--margin-above` or `--margin-below` modifier class |
+
+## Variants
+
+### Plain string description
+
+```tsx
+
+```
+
+### Description rendered above the field
+
+```tsx
+
+```
+
+### i18n locale map description
+
+```tsx
+
+```
diff --git a/packages/ui/src/fields/Number/Number.stories.tsx b/packages/ui/src/fields/Number/Number.stories.tsx
new file mode 100644
index 00000000000..0e4c7d70c89
--- /dev/null
+++ b/packages/ui/src/fields/Number/Number.stories.tsx
@@ -0,0 +1,123 @@
+'use client'
+import React, { useState } from 'react'
+
+import { InputStepper } from '../../elements/InputStepper/index.js'
+import { FieldDescription } from '../FieldDescription/index.js'
+import { FieldLabel } from '../FieldLabel/index.js'
+import { fieldBaseClass } from '../shared/index.js'
+
+export const meta = {
+ description: 'Numeric input with increment/decrement stepper controls.',
+ title: 'Fields / Number',
+}
+
+const baseClass = 'number'
+
+export const Default = () => {
+ const [value, setValue] = useState
('')
+ return (
+
+
+
+
+ setValue(e.target.value)}
+ step={1}
+ type="number"
+ value={value}
+ />
+ setValue((prev) => (prev === '' ? -1 : Number(prev) - 1))}
+ onIncrement={() => setValue((prev) => (prev === '' ? 1 : Number(prev) + 1))}
+ />
+
+
+
+ )
+}
+
+export const WithValue = () => {
+ const [value, setValue] = useState(29.99)
+ return (
+
+
+
+
+ setValue(e.target.value)}
+ step={0.01}
+ type="number"
+ value={value}
+ />
+ setValue((prev) => Math.max(0, Number(prev) - 0.01))}
+ onIncrement={() => setValue((prev) => Number(prev) + 0.01)}
+ />
+
+
+
+ )
+}
+
+export const WithMinMax = () => {
+ const [value, setValue] = useState(3)
+ return (
+
+
+
+
+
+ setValue(e.target.value)}
+ step={1}
+ type="number"
+ value={value}
+ />
+ setValue((prev) => Math.max(1, Number(prev) - 1))}
+ onIncrement={() => setValue((prev) => Math.min(5, Number(prev) + 1))}
+ />
+
+
+
+ )
+}
+
+export const ReadOnly = () => (
+
+
+
+
+ {}}
+ type="number"
+ value={1042}
+ />
+ {}} onIncrement={() => {}} />
+
+
+
+)
diff --git a/packages/ui/src/fields/Number/documentation.md b/packages/ui/src/fields/Number/documentation.md
new file mode 100644
index 00000000000..783c108e559
--- /dev/null
+++ b/packages/ui/src/fields/Number/documentation.md
@@ -0,0 +1,100 @@
+# NumberField
+
+The `NumberField` component is the fully-connected Payload number field. It renders a labelled `` accompanied by `InputStepper` increment/decrement buttons, and wires directly into the Payload form context via `useField`. The field enforces optional `min`, `max`, and `step` constraints, supports a `hasMany` tag mode for entering multiple numeric values, and handles its own validation, error, and description display.
+
+## Import
+
+```tsx
+import { NumberField } from '@payloadcms/ui'
+```
+
+## Usage
+
+The `NumberField` is a fully-connected field component. Use it as a custom field component in your Payload collection config:
+
+```tsx
+import { NumberField } from '@payloadcms/ui'
+
+// Inside a Payload collection admin config:
+{
+ name: 'price',
+ type: 'number',
+ admin: {
+ components: {
+ Field: NumberField,
+ },
+ },
+}
+```
+
+## Props
+
+`NumberField` implements the `NumberFieldClientComponent` contract from Payload. Configuration is drawn from the field config:
+
+| Prop / Admin Config | Type | Default | Description |
+| ------------------------- | --------------------------------------- | ----------- | ------------------------------------------------------------------- |
+| `field.label` | `string \| Record` | — | Label rendered above the input |
+| `field.required` | `boolean` | — | Marks the field as required |
+| `field.min` | `number` | `-Infinity` | Minimum allowed value; clamps stepper and validates |
+| `field.max` | `number` | `Infinity` | Maximum allowed value; clamps stepper and validates |
+| `field.hasMany` | `boolean` | `false` | Enables multi-value tag mode via `ReactSelect` |
+| `field.maxRows` | `number` | `Infinity` | Maximum number of values when `hasMany` is `true` |
+| `field.admin.step` | `number` | `1` | Increment/decrement amount used by the stepper and the native input |
+| `field.admin.placeholder` | `string \| Record` | — | Placeholder text; supports i18n locale map |
+| `field.admin.description` | `string \| (() => string) \| ReactNode` | — | Helper text rendered below the field |
+| `field.admin.className` | `string` | — | Additional CSS class for the root wrapper |
+| `readOnly` | `boolean` | — | Disables both the input and the stepper buttons |
+| `validate` | `function` | — | Custom validation function |
+| `onChange` | `(value: number) => void` | — | Optional external change callback |
+| `path` | `string` | — | Field path within the form data tree |
+
+## Variants
+
+### Integer quantity with min/max bounds
+
+```tsx
+{
+ name: 'quantity',
+ type: 'number',
+ label: 'Quantity',
+ min: 0,
+ max: 999,
+ admin: {
+ step: 1,
+ description: 'Available stock for this product.',
+ },
+}
+```
+
+### Decimal step for pricing
+
+```tsx
+{
+ name: 'price',
+ type: 'number',
+ label: 'Price (USD)',
+ min: 0,
+ admin: {
+ step: 0.01,
+ placeholder: '0.00',
+ description: 'Enter the price in US dollars.',
+ },
+}
+```
+
+### Multi-value numeric tags
+
+```tsx
+{
+ name: 'ratings',
+ type: 'number',
+ label: 'Ratings',
+ hasMany: true,
+ maxRows: 5,
+ min: 1,
+ max: 5,
+ admin: {
+ description: 'Enter up to 5 rating values between 1 and 5.',
+ },
+}
+```
diff --git a/packages/ui/src/fields/Point/Point.stories.tsx b/packages/ui/src/fields/Point/Point.stories.tsx
new file mode 100644
index 00000000000..6446e461590
--- /dev/null
+++ b/packages/ui/src/fields/Point/Point.stories.tsx
@@ -0,0 +1,154 @@
+'use client'
+import React, { useState } from 'react'
+
+import { InputStepper } from '../../elements/InputStepper/index.js'
+import { FieldDescription } from '../FieldDescription/index.js'
+import { FieldLabel } from '../FieldLabel/index.js'
+import { fieldBaseClass } from '../shared/index.js'
+
+export const meta = {
+ description: 'Longitude/latitude coordinate pair input for geographic point data.',
+ title: 'Fields / Point',
+}
+
+const baseClass = 'point'
+
+export const Default = () => {
+ const [longitude, setLongitude] = useState('')
+ const [latitude, setLatitude] = useState('')
+ return (
+
+ )
+}
+
+export const WithValue = () => {
+ const [longitude, setLongitude] = useState(-73.935242)
+ const [latitude, setLatitude] = useState(40.73061)
+ return (
+
+ )
+}
+
+export const ReadOnly = () => (
+
+)
diff --git a/packages/ui/src/fields/Point/documentation.md b/packages/ui/src/fields/Point/documentation.md
new file mode 100644
index 00000000000..222b440ca63
--- /dev/null
+++ b/packages/ui/src/fields/Point/documentation.md
@@ -0,0 +1,93 @@
+# PointField
+
+The `PointField` component is the fully-connected Payload geographic point field. It renders two labelled `` elements — one for longitude and one for latitude — each paired with an `InputStepper` for increment/decrement control. The pair of values is stored as a `[longitude, latitude]` tuple. The component wires directly into the Payload form context via `useField` and handles its own validation, error, and description display.
+
+## Import
+
+```tsx
+import { PointField } from '@payloadcms/ui'
+```
+
+## Usage
+
+The `PointField` is a fully-connected field component. Use it as a custom field component in your Payload collection config:
+
+```tsx
+import { PointField } from '@payloadcms/ui'
+
+// Inside a Payload collection admin config:
+{
+ name: 'location',
+ type: 'point',
+ admin: {
+ components: {
+ Field: PointField,
+ },
+ },
+}
+```
+
+## Props
+
+`PointField` implements the `PointFieldClientComponent` contract from Payload. Configuration is drawn from the field config:
+
+| Prop / Admin Config | Type | Default | Description |
+| ------------------------- | --------------------------------------- | ------- | ---------------------------------------------------------------------- |
+| `field.label` | `string \| Record` | — | Base label prepended to "Longitude" and "Latitude" sub-labels |
+| `field.required` | `boolean` | — | Marks both coordinate inputs as required |
+| `field.admin.step` | `number` | `1` | Increment/decrement amount for both stepper buttons |
+| `field.admin.placeholder` | `string \| Record` | — | Placeholder text for both coordinate inputs; supports i18n locale map |
+| `field.admin.description` | `string \| (() => string) \| ReactNode` | — | Helper text rendered below the field pair |
+| `field.admin.className` | `string` | — | Additional CSS class for the root wrapper |
+| `readOnly` | `boolean` | — | Disables both inputs and stepper buttons |
+| `validate` | `PointFieldValidation` | — | Custom validation function; receives the `[longitude, latitude]` tuple |
+| `path` | `string` | — | Field path within the form data tree |
+
+## Stored value format
+
+The field stores and receives values as a two-element tuple: `[longitude, latitude]`. Index `0` is longitude, index `1` is latitude — matching the GeoJSON coordinate order.
+
+## Variants
+
+### Basic location field
+
+```tsx
+{
+ name: 'storeLocation',
+ type: 'point',
+ label: 'Store Location',
+ admin: {
+ description: 'Enter the geographic coordinates of the store.',
+ },
+}
+```
+
+### High-precision coordinates with decimal step
+
+```tsx
+{
+ name: 'gpsCoordinates',
+ type: 'point',
+ label: 'GPS Coordinates',
+ admin: {
+ step: 0.000001,
+ placeholder: '0.000000',
+ description: 'Six decimal places provide ~0.1 m accuracy.',
+ },
+}
+```
+
+### Required read-only display
+
+```tsx
+{
+ name: 'originPoint',
+ type: 'point',
+ label: 'Origin',
+ required: true,
+ admin: {
+ readOnly: true,
+ description: 'Set automatically on creation.',
+ },
+}
+```
diff --git a/packages/ui/src/fields/Select/Select.stories.tsx b/packages/ui/src/fields/Select/Select.stories.tsx
new file mode 100644
index 00000000000..8ce37d3b279
--- /dev/null
+++ b/packages/ui/src/fields/Select/Select.stories.tsx
@@ -0,0 +1,114 @@
+'use client'
+import React, { useState } from 'react'
+
+import { SelectInput } from './Input.js'
+
+export const meta = {
+ description: 'Dropdown select for choosing from a fixed set of options.',
+ title: 'Fields / Select',
+}
+
+const statusOptions = [
+ { label: 'Draft', value: 'draft' },
+ { label: 'Published', value: 'published' },
+ { label: 'Archived', value: 'archived' },
+]
+
+const priorityOptions = [
+ { label: 'Low', value: 'low' },
+ { label: 'Medium', value: 'medium' },
+ { label: 'High', value: 'high' },
+ { label: 'Critical', value: 'critical' },
+]
+
+export const Default = () => {
+ const [value, setValue] = useState(null)
+ return (
+ setValue(selected ? (selected as { value: string }).value : null)}
+ options={statusOptions}
+ path="status"
+ value={value}
+ />
+ )
+}
+
+export const WithValue = () => {
+ const [value, setValue] = useState('published')
+ return (
+ setValue(selected ? (selected as { value: string }).value : null)}
+ options={statusOptions}
+ path="status"
+ value={value}
+ />
+ )
+}
+
+export const NotClearable = () => {
+ const [value, setValue] = useState('medium')
+ return (
+ setValue(selected ? (selected as { value: string }).value : null)}
+ options={priorityOptions}
+ path="priority"
+ value={value}
+ />
+ )
+}
+
+export const ReadOnly = () => (
+ {}}
+ options={statusOptions}
+ path="category"
+ readOnly
+ value="published"
+ />
+)
+
+export const WithError = () => {
+ const [value, setValue] = useState(null)
+ return (
+ setValue(selected ? (selected as { value: string }).value : null)}
+ options={statusOptions}
+ path="required"
+ required
+ showError
+ value={value}
+ />
+ )
+}
+
+export const MultiSelect = () => {
+ const [value, setValue] = useState(['low', 'high'])
+ return (
+ {
+ if (Array.isArray(selected)) {
+ setValue(selected.map((opt: { value: string }) => opt.value))
+ } else {
+ setValue([])
+ }
+ }}
+ options={priorityOptions}
+ path="tags"
+ value={value}
+ />
+ )
+}
diff --git a/packages/ui/src/fields/Select/documentation.md b/packages/ui/src/fields/Select/documentation.md
new file mode 100644
index 00000000000..c18bc5a5e43
--- /dev/null
+++ b/packages/ui/src/fields/Select/documentation.md
@@ -0,0 +1,114 @@
+# SelectInput
+
+The `SelectInput` component is the presentational layer for the Payload select field. It renders a labelled dropdown or multi-select backed by `ReactSelect`, with optional description and error state. Use this component directly for a standalone select outside of a Payload form context. The connected form component is `SelectField`.
+
+## Import
+
+```tsx
+import { SelectInput } from '@payloadcms/ui'
+```
+
+## Usage
+
+```tsx
+ setStatus((selected as { value: string }).value)}
+/>
+```
+
+## Props
+
+| Prop | Type | Default | Description |
+| --------------- | -------------------------------------------------------------- | ------- | ------------------------------------------------------------------------- |
+| `name` | `string` | — | **Required.** Native `name` passed through for form serialisation |
+| `path` | `string` | — | **Required.** Field path used to generate the wrapper `id` |
+| `options` | `{ label: string \| Record; value: string }[]` | — | Array of selectable options; labels support i18n locale maps |
+| `value` | `string \| string[]` | — | Controlled selected value(s) |
+| `onChange` | `ReactSelectAdapterProps['onChange']` | — | Called with the full react-select option object(s) on selection change |
+| `label` | `string \| Record` | — | Label rendered above the select via `FieldLabel` |
+| `hasMany` | `boolean` | `false` | Enables multi-select mode |
+| `isClearable` | `boolean` | `true` | Shows a clear button to reset the selection |
+| `isSortable` | `boolean` | `true` | Allows drag-to-reorder selected values in multi-select mode |
+| `readOnly` | `boolean` | — | Disables the select |
+| `required` | `boolean` | — | Marks the field as required and adds a visual indicator to the label |
+| `placeholder` | `string \| (() => string)` | — | Placeholder text when no option is selected |
+| `description` | `string \| (() => string) \| ReactNode` | — | Helper text rendered below the field via `FieldDescription` |
+| `showError` | `boolean` | — | When `true`, applies error styling and renders the `FieldError` component |
+| `localized` | `boolean` | — | Shows a localization indicator on the label |
+| `className` | `string` | — | Additional CSS class names for the root wrapper |
+| `id` | `string` | — | Additional `id` passed to the underlying `ReactSelect` instance |
+| `filterOption` | `ReactSelectAdapterProps['filterOption']` | — | Custom option filter function |
+| `onInputChange` | `ReactSelectAdapterProps['onInputChange']` | — | Called when the text in the search input changes |
+| `BeforeInput` | `ReactNode` | — | Slot rendered before the `ReactSelect` element |
+| `AfterInput` | `ReactNode` | — | Slot rendered after the `ReactSelect` element |
+| `Label` | `ReactNode` | — | Custom label component; overrides the default `FieldLabel` |
+| `Error` | `ReactNode` | — | Custom error component; overrides the default `FieldError` |
+| `Description` | `ReactNode` | — | Custom description component; overrides the default `FieldDescription` |
+| `style` | `React.CSSProperties` | — | Inline styles applied to the root wrapper |
+
+## Variants
+
+### Single-select with placeholder
+
+```tsx
+ setCategory((selected as { value: string }).value)}
+/>
+```
+
+### Multi-select with description
+
+```tsx
+ setTags((selected as { value: string }[]).map((opt) => opt.value))}
+/>
+```
+
+### Read-only select
+
+```tsx
+ {}}
+/>
+```
diff --git a/packages/ui/src/fields/Text/Text.stories.tsx b/packages/ui/src/fields/Text/Text.stories.tsx
new file mode 100644
index 00000000000..060cc987eeb
--- /dev/null
+++ b/packages/ui/src/fields/Text/Text.stories.tsx
@@ -0,0 +1,77 @@
+'use client'
+import React, { useState } from 'react'
+
+import { TextInput } from './Input.js'
+
+export const meta = {
+ description: 'Single-line text input for short string values.',
+ title: 'Fields / Text',
+}
+
+export const Default = () => {
+ const [value, setValue] = useState('')
+ return (
+ setValue(e.target.value)}
+ path="title"
+ value={value}
+ />
+ )
+}
+
+export const WithValue = () => {
+ const [value, setValue] = useState('my-document-slug')
+ return (
+ setValue(e.target.value)} path="slug" value={value} />
+ )
+}
+
+export const WithPlaceholder = () => {
+ const [value, setValue] = useState('')
+ return (
+ setValue(e.target.value)}
+ path="email"
+ placeholder="e.g. hello@example.com"
+ value={value}
+ />
+ )
+}
+
+export const ReadOnly = () => (
+ {}}
+ path="readOnly"
+ readOnly
+ value="Cannot edit this"
+ />
+)
+
+export const WithError = () => {
+ const [value, setValue] = useState('')
+ return (
+ setValue(e.target.value)}
+ path="required"
+ showError
+ value={value}
+ />
+ )
+}
+
+export const Required = () => {
+ const [value, setValue] = useState('')
+ return (
+ setValue(e.target.value)}
+ path="title"
+ required
+ value={value}
+ />
+ )
+}
diff --git a/packages/ui/src/fields/Text/documentation.md b/packages/ui/src/fields/Text/documentation.md
new file mode 100644
index 00000000000..28a22f92068
--- /dev/null
+++ b/packages/ui/src/fields/Text/documentation.md
@@ -0,0 +1,74 @@
+# TextInput
+
+The `TextInput` component is the presentational layer for the Payload text field. It renders a labelled single-line `` with optional description, error state, and slot nodes for custom content before and after the input. Use this component directly when you need a standalone, uncontrolled text input outside of a Payload form context.
+
+## Import
+
+```tsx
+import { TextInput } from '@payloadcms/ui'
+```
+
+## Usage
+
+```tsx
+ setValue(e.target.value)} />
+```
+
+## Props
+
+| Prop | Type | Default | Description |
+| ------------- | -------------------------------------------- | ------- | ---------------------------------------------------------------------------- |
+| `path` | `string` | — | **Required.** Field path used as the `name` and `id` attributes on the input |
+| `label` | `string \| Record` | — | Field label rendered above the input via `FieldLabel` |
+| `value` | `string` | — | Controlled value of the input |
+| `onChange` | `(e: ChangeEvent) => void` | — | Change handler for single-value mode |
+| `readOnly` | `boolean` | — | Disables the input when `true` |
+| `required` | `boolean` | — | Marks the field as required and adds a visual indicator to the label |
+| `placeholder` | `string \| Record` | — | Placeholder text; supports i18n locale map |
+| `description` | `string \| (() => string) \| ReactNode` | — | Helper text rendered below the input via `FieldDescription` |
+| `showError` | `boolean` | — | When `true`, applies error styling and renders the `FieldError` component |
+| `className` | `string` | — | Additional CSS class names for the root wrapper |
+| `localized` | `boolean` | — | Shows a localization indicator on the label |
+| `rtl` | `boolean` | — | Sets `data-rtl` on the input to enable right-to-left text direction |
+| `hasMany` | `boolean` | — | Switches to multi-value tag mode backed by `ReactSelect` |
+| `maxRows` | `number` | — | Maximum number of tags when `hasMany` is `true` |
+| `BeforeInput` | `ReactNode` | — | Slot rendered immediately before the `` element |
+| `AfterInput` | `ReactNode` | — | Slot rendered immediately after the `` element |
+| `Label` | `ReactNode` | — | Custom label component; overrides the default `FieldLabel` |
+| `Error` | `ReactNode` | — | Custom error component; overrides the default `FieldError` |
+| `Description` | `ReactNode` | — | Custom description component; overrides the default `FieldDescription` |
+| `inputRef` | `React.RefObject` | — | Ref forwarded to the underlying `` element |
+| `style` | `React.CSSProperties` | — | Inline styles applied to the root wrapper |
+
+## Variants
+
+### Required field with description
+
+```tsx
+ setSeoTitle(e.target.value)}
+/>
+```
+
+### Read-only display
+
+```tsx
+
+```
+
+### Field in error state
+
+```tsx
+ setUsername(e.target.value)}
+/>
+```
diff --git a/packages/ui/src/fields/Textarea/Textarea.stories.tsx b/packages/ui/src/fields/Textarea/Textarea.stories.tsx
new file mode 100644
index 00000000000..666466e16d1
--- /dev/null
+++ b/packages/ui/src/fields/Textarea/Textarea.stories.tsx
@@ -0,0 +1,84 @@
+'use client'
+import React, { useState } from 'react'
+
+import { TextareaInput } from './Input.js'
+
+export const meta = {
+ description: 'Multi-line text input for longer string values.',
+ title: 'Fields / Textarea',
+}
+
+export const Default = () => {
+ const [value, setValue] = useState('')
+ return (
+ setValue(e.target.value)}
+ path="description"
+ value={value}
+ />
+ )
+}
+
+export const WithValue = () => {
+ const [value, setValue] = useState(
+ 'This is some longer content that spans multiple lines and describes the document in detail.',
+ )
+ return (
+ setValue(e.target.value)}
+ path="body"
+ value={value}
+ />
+ )
+}
+
+export const WithPlaceholder = () => {
+ const [value, setValue] = useState('')
+ return (
+ setValue(e.target.value)}
+ path="summary"
+ placeholder="Enter a brief summary..."
+ value={value}
+ />
+ )
+}
+
+export const WithCustomRows = () => {
+ const [value, setValue] = useState('')
+ return (
+ setValue(e.target.value)}
+ path="notes"
+ rows={8}
+ value={value}
+ />
+ )
+}
+
+export const ReadOnly = () => (
+ {}}
+ path="readOnly"
+ readOnly
+ value="This content cannot be edited by the user."
+ />
+)
+
+export const Required = () => {
+ const [value, setValue] = useState('')
+ return (
+ setValue(e.target.value)}
+ path="notes"
+ required
+ value={value}
+ />
+ )
+}
diff --git a/packages/ui/src/fields/Textarea/documentation.md b/packages/ui/src/fields/Textarea/documentation.md
new file mode 100644
index 00000000000..393fb21a997
--- /dev/null
+++ b/packages/ui/src/fields/Textarea/documentation.md
@@ -0,0 +1,73 @@
+# TextareaInput
+
+The `TextareaInput` component is the presentational layer for the Payload textarea field. It renders a labelled `