From 072adb37f734864a8d04c30482df04cf84dbce1e Mon Sep 17 00:00:00 2001 From: Reid Barber Date: Wed, 7 Jan 2026 12:14:38 -0600 Subject: [PATCH 1/5] add OTP examples to React Aria TextField docs --- .../s2-docs/pages/react-aria/TextField.mdx | 143 +++++++++++++++++- 1 file changed, 142 insertions(+), 1 deletion(-) diff --git a/packages/dev/s2-docs/pages/react-aria/TextField.mdx b/packages/dev/s2-docs/pages/react-aria/TextField.mdx index 2641f48bbe3..e002691e13c 100644 --- a/packages/dev/s2-docs/pages/react-aria/TextField.mdx +++ b/packages/dev/s2-docs/pages/react-aria/TextField.mdx @@ -8,7 +8,7 @@ import {TextField as TailwindTextField} from 'tailwind-starter/TextField'; import '../../tailwind/tailwind.css'; import Anatomy from '@react-aria/textfield/docs/anatomy.svg'; -export const tags = ['input']; +export const tags = ['input', 'otp']; export const relatedPages = [{'title': 'useTextField', 'url': 'TextField/useTextField.html'}]; export const description = 'Allows a user to enter a plain text value with a keyboard.'; @@ -106,6 +106,147 @@ import {TextField, Label, TextArea} from 'react-aria-components'; ``` +## OTP Input + +TextField can be customized to create an OTP (one-time password) input by hiding the actual input and rendering styled character boxes instead. This uses `InputContext` to access the current value and render each character in a separate box. + + + ```tsx render type="vanilla" + "use client"; + import {TextField, Input, InputContext} from 'react-aria-components'; + import {useState, useContext} from 'react'; + + function OTPInput(props) { + let [focused, setFocused] = useState(false); + let length = props.length ?? 6; + + return ( + + + setFocused(true)} + onBlur={() => setFocused(false)} + inputMode="numeric" + pattern="[0-9]*" + autoComplete="one-time-code" + style={{ + position: 'absolute', + inset: 0, + width: '100%', + height: '100%', + opacity: 0, + pointerEvents: 'auto', + cursor: 'text' + }} + /> + + ); + } + + function OTPBoxes({length, focused}) { + let context = useContext(InputContext); + let value = context?.value ?? ''; + + return ( +
+ {Array.from({length}, (_, i) => ( +
+ {value[i] ?? ''} +
+ ))} +
+ ); + } + + + ``` + + ```tsx render type="tailwind" + "use client"; + import {TextField, Input, InputContext} from 'react-aria-components'; + import {useState, useContext} from 'react'; + + function OTPInput(props) { + let [focused, setFocused] = useState(false); + let length = props.length ?? 6; + + return ( + + + setFocused(true)} + onBlur={() => setFocused(false)} + inputMode="numeric" + pattern="[0-9]*" + autoComplete="one-time-code" + className="absolute inset-0 w-full h-full opacity-0 cursor-text" + /> + + ); + } + + function OTPBoxes({length, focused}) { + let context = useContext(InputContext); + let value = context?.value ?? ''; + + return ( +
+ {Array.from({length}, (_, i) => ( +
+ {value[i] ?? ''} +
+ ))} +
+ ); + } + + + ``` + +
+ ## API From 07812f4c043c8935f8350827192d7f8c1fed1939 Mon Sep 17 00:00:00 2001 From: Reid Barber Date: Wed, 7 Jan 2026 12:24:46 -0600 Subject: [PATCH 2/5] hide caret --- packages/dev/s2-docs/pages/react-aria/TextField.mdx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/dev/s2-docs/pages/react-aria/TextField.mdx b/packages/dev/s2-docs/pages/react-aria/TextField.mdx index e002691e13c..d2235f3f263 100644 --- a/packages/dev/s2-docs/pages/react-aria/TextField.mdx +++ b/packages/dev/s2-docs/pages/react-aria/TextField.mdx @@ -144,7 +144,8 @@ TextField can be customized to create an OTP (one-time password) input by hiding height: '100%', opacity: 0, pointerEvents: 'auto', - cursor: 'text' + cursor: 'text', + caretColor: 'transparent' }} /> @@ -209,7 +210,7 @@ TextField can be customized to create an OTP (one-time password) input by hiding inputMode="numeric" pattern="[0-9]*" autoComplete="one-time-code" - className="absolute inset-0 w-full h-full opacity-0 cursor-text" + className="absolute inset-0 w-full h-full opacity-0 cursor-text caret-transparent" /> ); From 58da985f45b7b1bd14d668ccf4fbcc3a49202dba Mon Sep 17 00:00:00 2001 From: Reid Barber Date: Wed, 7 Jan 2026 12:27:04 -0600 Subject: [PATCH 3/5] typescript --- packages/dev/s2-docs/pages/react-aria/TextField.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/dev/s2-docs/pages/react-aria/TextField.mdx b/packages/dev/s2-docs/pages/react-aria/TextField.mdx index d2235f3f263..16866c95376 100644 --- a/packages/dev/s2-docs/pages/react-aria/TextField.mdx +++ b/packages/dev/s2-docs/pages/react-aria/TextField.mdx @@ -154,7 +154,7 @@ TextField can be customized to create an OTP (one-time password) input by hiding function OTPBoxes({length, focused}) { let context = useContext(InputContext); - let value = context?.value ?? ''; + let value = String(context?.value ?? ''); return (
@@ -218,7 +218,7 @@ TextField can be customized to create an OTP (one-time password) input by hiding function OTPBoxes({length, focused}) { let context = useContext(InputContext); - let value = context?.value ?? ''; + let value = String(context?.value ?? ''); return (
From 56bb76cdfe4699b1c4a22cc0bee194cda4d38148 Mon Sep 17 00:00:00 2001 From: Reid Barber Date: Tue, 13 Jan 2026 17:32:23 -0600 Subject: [PATCH 4/5] keep focus visible on last item when filled --- packages/dev/s2-docs/pages/react-aria/TextField.mdx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/dev/s2-docs/pages/react-aria/TextField.mdx b/packages/dev/s2-docs/pages/react-aria/TextField.mdx index 16866c95376..22b88189c34 100644 --- a/packages/dev/s2-docs/pages/react-aria/TextField.mdx +++ b/packages/dev/s2-docs/pages/react-aria/TextField.mdx @@ -173,9 +173,9 @@ TextField can be customized to create an OTP (one-time password) input by hiding borderRadius: 'var(--radius)', background: 'var(--field-background)', border: '1px solid var(--border-color)', - boxShadow: focused && value.length === i - ? '0 0 0 2px var(--focus-ring-color)' - : 'none', + boxShadow: focused && (value.length === i || (i === length - 1 && value.length >= length)) + ? '0 0 0 2px var(--focus-ring-color)' + : 'none', color: 'var(--field-text-color)', transition: 'box-shadow 150ms' }}> @@ -232,7 +232,7 @@ TextField can be customized to create an OTP (one-time password) input by hiding border border-gray-300 dark:border-zinc-600 text-gray-900 dark:text-zinc-100 transition-shadow duration-150 - ${focused && value.length === i + ${focused && (value.length === i || (i === length - 1 && value.length >= length)) ? 'ring-2 ring-blue-600 dark:ring-blue-500' : ''} `}> From 9e37220770ffcd9798232f95db04889e38d61b06 Mon Sep 17 00:00:00 2001 From: Reid Barber Date: Tue, 13 Jan 2026 17:39:22 -0600 Subject: [PATCH 5/5] cleanup tailwind to prevent horizontal scroll in docs --- .../s2-docs/pages/react-aria/TextField.mdx | 41 +++++++++++-------- 1 file changed, 23 insertions(+), 18 deletions(-) diff --git a/packages/dev/s2-docs/pages/react-aria/TextField.mdx b/packages/dev/s2-docs/pages/react-aria/TextField.mdx index 22b88189c34..d58aca35d0c 100644 --- a/packages/dev/s2-docs/pages/react-aria/TextField.mdx +++ b/packages/dev/s2-docs/pages/react-aria/TextField.mdx @@ -210,7 +210,8 @@ TextField can be customized to create an OTP (one-time password) input by hiding inputMode="numeric" pattern="[0-9]*" autoComplete="one-time-code" - className="absolute inset-0 w-full h-full opacity-0 cursor-text caret-transparent" + className="absolute inset-0 w-full h-full + opacity-0 cursor-text caret-transparent" /> ); @@ -222,23 +223,27 @@ TextField can be customized to create an OTP (one-time password) input by hiding return (
- {Array.from({length}, (_, i) => ( -
= length)) - ? 'ring-2 ring-blue-600 dark:ring-blue-500' - : ''} - `}> - {value[i] ?? ''} -
- ))} + {Array.from({length}, (_, i) => { + let isAtCaret = value.length === i; + let isLastFilled = i === length - 1 && value.length >= length; + return ( +
+ {value[i] ?? ''} +
+ ); + })}
); }