diff --git a/apps/www/src/components/playground/index.ts b/apps/www/src/components/playground/index.ts
index 3f1241e24..5b8d48e08 100644
--- a/apps/www/src/components/playground/index.ts
+++ b/apps/www/src/components/playground/index.ts
@@ -36,6 +36,7 @@ export * from './list-examples';
export * from './menu-examples';
export * from './menubar-examples';
export * from './number-field-examples';
+export * from './otp-field-examples';
export * from './popover-examples';
export * from './preview-card-examples';
export * from './radio-examples';
diff --git a/apps/www/src/components/playground/otp-field-examples.tsx b/apps/www/src/components/playground/otp-field-examples.tsx
new file mode 100644
index 000000000..0fb7dead6
--- /dev/null
+++ b/apps/www/src/components/playground/otp-field-examples.tsx
@@ -0,0 +1,121 @@
+'use client';
+
+import { Field, Flex, OTPField, Text } from '@raystack/apsara';
+import { useState } from 'react';
+import PlaygroundLayout from './playground-layout';
+
+const renderSlots = (length: number, offset = 0, totalLength = length) =>
+ Array.from({ length }, (_, i) => (
+
+ ));
+
+function ControlledOTP() {
+ const [value, setValue] = useState('');
+ return (
+
+
+ {renderSlots(6)}
+
+
+ Current value: {value || '(empty)'}
+
+
+ );
+}
+
+function CompleteOTP() {
+ const [submitted, setSubmitted] = useState('');
+ return (
+
+
+ {renderSlots(6)}
+
+
+ {submitted ? `Submitted: ${submitted}` : 'Type all 6 digits to submit'}
+
+
+ );
+}
+
+export function OTPFieldExamples() {
+ return (
+
+
+ Default (6 digits):
+ {renderSlots(6)}
+
+
+
+ 4 digits:
+ {renderSlots(4)}
+
+
+
+ With separator:
+
+ {renderSlots(3, 0, 6)}
+
+ {renderSlots(3, 3, 6)}
+
+
+
+
+ Masked:
+
+ {renderSlots(6)}
+
+
+
+
+ Alphanumeric:
+
+ {renderSlots(6)}
+
+
+
+
+ Default value:
+
+ {renderSlots(6)}
+
+
+
+
+ Controlled (value / onValueChange):
+
+
+
+
+ onValueComplete:
+
+
+
+
+ Disabled:
+
+ {renderSlots(6)}
+
+
+
+
+ Read-only:
+
+ {renderSlots(6)}
+
+
+
+
+ With Field:
+
+ {renderSlots(6)}
+
+
+
+ );
+}
diff --git a/apps/www/src/content/docs/components/otp-field/demo.ts b/apps/www/src/content/docs/components/otp-field/demo.ts
new file mode 100644
index 000000000..b772e66f7
--- /dev/null
+++ b/apps/www/src/content/docs/components/otp-field/demo.ts
@@ -0,0 +1,182 @@
+'use client';
+
+import type { ComponentPropsType } from '@/components/demo/types';
+import { getPropsString } from '@/lib/utils';
+
+const renderInputs = (
+ length: number
+) => `Array.from({ length: ${length} }, (_, i) => (
+
+ ))`;
+
+export const preview = {
+ type: 'code',
+ code: `
+ {${renderInputs(6)}}
+`
+};
+
+const getCode = (props: ComponentPropsType) => {
+ const { length = 6, ...rest } = props;
+ const slotCount = Number(length) || 6;
+ return `
+ {${renderInputs(slotCount)}}
+`;
+};
+
+export const playground = {
+ type: 'playground',
+ controls: {
+ length: {
+ type: 'select',
+ options: ['4', '6', '8'],
+ defaultValue: '6'
+ },
+ validationType: {
+ type: 'select',
+ options: ['numeric', 'alpha', 'alphanumeric', 'none'],
+ defaultValue: 'numeric'
+ },
+ mask: {
+ type: 'checkbox',
+ defaultValue: false
+ },
+ disabled: {
+ type: 'checkbox',
+ defaultValue: false
+ },
+ readOnly: {
+ type: 'checkbox',
+ defaultValue: false
+ },
+ autoSubmit: {
+ type: 'checkbox',
+ defaultValue: false
+ }
+ },
+ getCode
+};
+
+export const separatorDemo = {
+ type: 'code',
+ code: `
+ {Array.from({ length: 3 }, (_, i) => (
+
+ ))}
+
+ {Array.from({ length: 3 }, (_, i) => (
+
+ ))}
+`
+};
+
+export const maskedDemo = {
+ type: 'code',
+ code: `
+ {Array.from({ length: 6 }, (_, i) => (
+
+ ))}
+`
+};
+
+export const alphanumericDemo = {
+ type: 'code',
+ code: `
+ {Array.from({ length: 6 }, (_, i) => (
+
+ ))}
+`
+};
+
+export const disabledDemo = {
+ type: 'code',
+ code: `
+ {Array.from({ length: 6 }, (_, i) => (
+
+ ))}
+`
+};
+
+export const readOnlyDemo = {
+ type: 'code',
+ code: `
+ {Array.from({ length: 6 }, (_, i) => (
+
+ ))}
+`
+};
+
+export const controlledDemo = {
+ type: 'code',
+ code: `function ControlledOTP() {
+ const [value, setValue] = React.useState('');
+
+ return (
+
+
+ {Array.from({ length: 6 }, (_, i) => (
+
+ ))}
+
+ Current value: {value || '(empty)'}
+
+ );
+}`
+};
+
+export const onCompleteDemo = {
+ type: 'code',
+ code: `function CompleteOTP() {
+ const [submitted, setSubmitted] = React.useState('');
+
+ return (
+
+ setSubmitted(value)}
+ >
+ {Array.from({ length: 6 }, (_, i) => (
+
+ ))}
+
+
+ {submitted ? \`Submitted: \${submitted}\` : 'Type all 6 digits to submit'}
+
+
+ );
+}`
+};
+
+export const customSanitizeDemo = {
+ type: 'code',
+ code: ` val.replace(/[^0-3]/g, '')}
+>
+ {Array.from({ length: 4 }, (_, i) => (
+
+ ))}
+`
+};
+
+export const withFieldDemo = {
+ type: 'code',
+ code: `
+
+
+ {Array.from({ length: 6 }, (_, i) => (
+
+ ))}
+
+
+
+
+ {Array.from({ length: 6 }, (_, i) => (
+
+ ))}
+
+
+`
+};
diff --git a/apps/www/src/content/docs/components/otp-field/index.mdx b/apps/www/src/content/docs/components/otp-field/index.mdx
new file mode 100644
index 000000000..1e5e2f2ed
--- /dev/null
+++ b/apps/www/src/content/docs/components/otp-field/index.mdx
@@ -0,0 +1,123 @@
+---
+title: OTP Field
+description: A one-time password input split into individual character slots with automatic focus management.
+source: packages/raystack/components/otp-field
+tag: new
+---
+
+import {
+ playground,
+ separatorDemo,
+ maskedDemo,
+ alphanumericDemo,
+ disabledDemo,
+ readOnlyDemo,
+ controlledDemo,
+ onCompleteDemo,
+ customSanitizeDemo,
+ withFieldDemo,
+} from "./demo.ts";
+
+
+
+## Anatomy
+
+Import and assemble the component. `length` is required so the field can manage state, validate input, and detect completion.
+
+```tsx
+import { OTPField } from "@raystack/apsara";
+
+
+
+
+
+
+
+
+
+
+```
+
+## API Reference
+
+### Root
+
+Groups all parts of the field and manages their state.
+
+
+
+### Input
+
+An individual character slot. Render one per slot (typically using `Array.from`).
+
+
+
+### Separator
+
+A visual separator between slot groups, styled to fit between OTP slots.
+
+
+
+## Examples
+
+### With separator
+
+Group slots visually with `OTPField.Separator` to make long codes easier to read.
+
+
+
+### Masked
+
+Use `mask` to obscure entered characters — useful for sensitive codes.
+
+
+
+### Alphanumeric
+
+Use `validationType` to accept letters, digits, or both. Defaults to `"numeric"`.
+
+
+
+### Disabled
+
+Set `disabled` to prevent interaction.
+
+
+
+### Read-only
+
+Set `readOnly` to display a value without allowing edits.
+
+
+
+### Controlled
+
+Pass `value` and `onValueChange` to control the field from React state.
+
+
+
+### Complete callback
+
+`onValueComplete` fires once all slots are filled. Combine with `autoSubmit` to submit the surrounding form automatically.
+
+
+
+### Custom sanitization
+
+Set `validationType="none"` and provide `sanitizeValue` to restrict input to a custom set of characters.
+
+
+
+### With Field
+
+Compose with `Field` to get an associated label and description.
+
+
+
+## Accessibility
+
+- Each slot must have an accessible name. Use a wrapping `