Skip to content

Commit 5a6e9b3

Browse files
committed
feat: add animated otp to examples
1 parent 1e98d2e commit 5a6e9b3

File tree

7 files changed

+458
-42
lines changed

7 files changed

+458
-42
lines changed
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
---
2+
title: Animated Stripe
3+
description: React Native OTPInput with animated Stripe style
4+
head:
5+
- tag: title
6+
content: Animated Stripe OTPInput | React Native OTPInput with animated Stripe style
7+
---
8+
9+
import { Code } from '@astrojs/starlight/components';
10+
import animatedStripeCode from '@example/src/examples/animated-stripe-nativewind.tsx?raw';
11+
12+
## Animated Stripe Style
13+
14+
This example demonstrates an animated version of the Stripe-style OTP input with:
15+
16+
- **Fade-in animations** for entered characters
17+
- **NativeWind styling** for consistent design
18+
19+
> Make sure you have `react-native-reanimated` and Nativewind installed and configured in your project.
20+
21+
<Code
22+
code={animatedStripeCode}
23+
lang="tsx"
24+
title="animated-stripe-nativewind.tsx"
25+
/>

docs/src/content/docs/getting-started.mdx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import { Code } from '@astrojs/starlight/components';
3838
We create a few examples that you can copy paste and use in your project.
3939

4040
- [Stripe style](/examples/stripe)
41+
- [Animated Stripe style](/examples/animated-stripe)
4142
- [Apple style](/examples/apple)
4243
- [Revolt style](/examples/revolt)
4344
- [Dashed style](/examples/dashed)

example/src/App.tsx

Lines changed: 141 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,155 @@
1-
import { View, StyleSheet, ScrollView, SafeAreaView } from 'react-native';
1+
import {
2+
View,
3+
ScrollView,
4+
SafeAreaView,
5+
Text,
6+
TouchableOpacity,
7+
} from 'react-native';
8+
import { useState } from 'react';
29
import StripeOTPInput from './examples/stripe';
3-
import { Title } from './title';
410
import AppleOTPInput from './examples/apple';
511
import RevoltOTPInput from './examples/revolt';
612
import DashedOTPInput from './examples/dashed';
713
import StripeNativewind from './examples/stripe-nativewind';
814
import RevoltNativewind from './examples/revolt-nativewind';
915
import AppleNativewind from './examples/apple-nativewind';
1016
import DashedNativewind from './examples/dashed-nativewind';
17+
import AnimatedDashedNativewind from './examples/animated-dashed-nativewind';
18+
import AnimatedStripeOTPInput from './examples/animated-stripe-nativewind';
19+
20+
type TabType = 'regular' | 'nativewind';
1121

1222
export default function App() {
23+
const [activeTab, setActiveTab] = useState<TabType>('regular');
24+
25+
const examples = [
26+
{
27+
title: 'Stripe',
28+
description: 'Clean, minimal design with subtle borders',
29+
regular: StripeOTPInput,
30+
nativewind: StripeNativewind,
31+
color: '#6772E5',
32+
},
33+
{
34+
title: 'Apple',
35+
description: 'iOS-style with rounded corners and shadows',
36+
regular: AppleOTPInput,
37+
nativewind: AppleNativewind,
38+
color: '#007AFF',
39+
},
40+
{
41+
title: 'Revolt',
42+
description: 'Modern design with gradient backgrounds',
43+
regular: RevoltOTPInput,
44+
nativewind: RevoltNativewind,
45+
color: '#FF6B6B',
46+
},
47+
{
48+
title: 'Dashed',
49+
description: 'Simple dashed border style',
50+
regular: DashedOTPInput,
51+
nativewind: DashedNativewind,
52+
color: '#4ECDC4',
53+
},
54+
{
55+
title: 'Animated Dashed',
56+
description: 'Dashed style with slide-in animation',
57+
regular: AnimatedDashedNativewind,
58+
nativewind: AnimatedDashedNativewind,
59+
color: '#8B5CF6',
60+
},
61+
{
62+
title: 'Animated Stripe',
63+
description: 'Stripe style with slide-in animation',
64+
regular: AnimatedStripeOTPInput,
65+
nativewind: AnimatedStripeOTPInput,
66+
color: '#8B5CF6',
67+
},
68+
];
69+
1370
return (
14-
<ScrollView style={styles.container}>
15-
<SafeAreaView>
16-
<View style={styles.exampleContainer}>
17-
<Title>Stripe OTP Input</Title>
18-
<StripeOTPInput />
19-
<StripeNativewind />
20-
</View>
21-
<View style={styles.exampleContainer}>
22-
<Title>Apple OTP Input</Title>
23-
<AppleOTPInput />
24-
<AppleNativewind />
25-
</View>
26-
<View style={styles.exampleContainer}>
27-
<Title>Revolt OTP Input</Title>
28-
<RevoltOTPInput />
29-
<RevoltNativewind />
30-
</View>
31-
<View style={styles.exampleContainer}>
32-
<Title>Dashed OTP Input</Title>
33-
<DashedOTPInput />
34-
<DashedNativewind />
71+
<SafeAreaView className="flex-1 bg-white">
72+
<View className="px-5 pt-5 pb-4 bg-white">
73+
<Text className="text-3xl font-bold text-slate-800 mb-1">
74+
OTP Input Examples
75+
</Text>
76+
<Text className="text-base text-slate-500 leading-6">
77+
Beautiful, customizable one-time password inputs
78+
</Text>
79+
</View>
80+
81+
<View className="flex-row bg-white px-5 pb-4 border-b border-slate-200">
82+
<TouchableOpacity
83+
className={`flex-1 py-3 px-4 mx-1 rounded-lg ${
84+
activeTab === 'regular' ? 'bg-blue-500' : 'bg-slate-100'
85+
}`}
86+
onPress={() => setActiveTab('regular')}
87+
>
88+
<Text
89+
className={`text-center text-sm font-semibold ${
90+
activeTab === 'regular' ? 'text-white' : 'text-slate-500'
91+
}`}
92+
>
93+
StyleSheet
94+
</Text>
95+
</TouchableOpacity>
96+
<TouchableOpacity
97+
className={`flex-1 py-3 px-4 mx-1 rounded-lg ${
98+
activeTab === 'nativewind' ? 'bg-blue-500' : 'bg-slate-100'
99+
}`}
100+
onPress={() => setActiveTab('nativewind')}
101+
>
102+
<Text
103+
className={`text-center text-sm font-semibold ${
104+
activeTab === 'nativewind' ? 'text-white' : 'text-slate-500'
105+
}`}
106+
>
107+
NativeWind
108+
</Text>
109+
</TouchableOpacity>
110+
</View>
111+
112+
<ScrollView className="flex-1" showsVerticalScrollIndicator={false}>
113+
{examples.map((example) => (
114+
<View
115+
key={example.title}
116+
className="mx-5 mt-5 bg-white rounded-2xl shadow-sm"
117+
>
118+
<View className="flex-row items-center px-4 py-2 border-b border-slate-200">
119+
<View
120+
className="w-10 h-10 rounded-full items-center justify-center mr-3"
121+
style={{ backgroundColor: example.color }}
122+
>
123+
<Text className="text-white text-lg font-bold">
124+
{example.title[0]}
125+
</Text>
126+
</View>
127+
<View className="flex-1">
128+
<Text className="text-lg font-semibold text-slate-800 mb-1">
129+
{example.title}
130+
</Text>
131+
<Text className="text-sm text-slate-500 leading-5">
132+
{example.description}
133+
</Text>
134+
</View>
135+
</View>
136+
137+
<View className="bg-slate-50 rounded-b-2xl p-4 items-center h-[120px] justify-center">
138+
{activeTab === 'regular' ? (
139+
<example.regular />
140+
) : (
141+
<example.nativewind />
142+
)}
143+
</View>
144+
</View>
145+
))}
146+
147+
<View className="py-10 items-center">
148+
<Text className="text-sm text-slate-400 text-center">
149+
Built with ❤️ using input-otp-native
150+
</Text>
35151
</View>
36-
</SafeAreaView>
37-
</ScrollView>
152+
</ScrollView>
153+
</SafeAreaView>
38154
);
39155
}
40-
41-
const styles = StyleSheet.create({
42-
container: {
43-
flex: 1,
44-
// justifyContent: 'center',
45-
},
46-
exampleContainer: {
47-
paddingVertical: 16,
48-
borderBottomWidth: 1,
49-
borderColor: '#E5E7EB',
50-
borderRadius: 8,
51-
},
52-
});
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import { View, Text, Alert } from 'react-native';
2+
import { OTPInput, type SlotProps } from 'input-otp-native';
3+
import type { OTPInputRef } from 'input-otp-native';
4+
import { useRef } from 'react';
5+
6+
import Animated, {
7+
useAnimatedStyle,
8+
withRepeat,
9+
withTiming,
10+
withSequence,
11+
useSharedValue,
12+
FadeInDown,
13+
} from 'react-native-reanimated';
14+
import { useEffect } from 'react';
15+
import { cn } from './utils';
16+
17+
export default function AnimatedDashedOTPInput() {
18+
const ref = useRef<OTPInputRef>(null);
19+
const onComplete = (code: string) => {
20+
Alert.alert('Completed with code:', code);
21+
ref.current?.clear();
22+
};
23+
24+
return (
25+
<View>
26+
<OTPInput
27+
ref={ref}
28+
onComplete={onComplete}
29+
maxLength={5}
30+
render={({ slots }) => (
31+
<View className="flex-row items-center justify-center my-4">
32+
{slots.map((slot, idx) => (
33+
<Slot key={idx} {...slot} />
34+
))}
35+
</View>
36+
)}
37+
/>
38+
</View>
39+
);
40+
}
41+
42+
function Slot({ char, isActive, hasFakeCaret }: SlotProps) {
43+
return (
44+
<View className="w-12 h-12 mx-2 items-center justify-center">
45+
{char !== null && (
46+
<Animated.View
47+
entering={FadeInDown.springify()
48+
.damping(15)
49+
.stiffness(150)
50+
.mass(1)
51+
.overshootClamping(0)}
52+
>
53+
<Text className="text-3xl font-medium text-gray-900">{char}</Text>
54+
</Animated.View>
55+
)}
56+
{hasFakeCaret && <FakeCaret />}
57+
<View
58+
className={cn('absolute bottom-0 w-full h-[1px] bg-gray-200', {
59+
'bg-gray-900 h-0.5': isActive,
60+
})}
61+
/>
62+
</View>
63+
);
64+
}
65+
66+
function FakeCaret() {
67+
const opacity = useSharedValue(1);
68+
69+
useEffect(() => {
70+
opacity.value = withRepeat(
71+
withSequence(
72+
withTiming(0, { duration: 500 }),
73+
withTiming(1, { duration: 500 })
74+
),
75+
-1,
76+
true
77+
);
78+
}, [opacity]);
79+
80+
const animatedStyle = useAnimatedStyle(() => ({
81+
opacity: opacity.value,
82+
}));
83+
84+
const baseStyle = {
85+
width: 2,
86+
height: 24,
87+
backgroundColor: '#111827',
88+
borderRadius: 1,
89+
};
90+
91+
return (
92+
<View className="absolute w-full h-full items-center justify-center">
93+
<Animated.View style={[baseStyle, animatedStyle]} />
94+
</View>
95+
);
96+
}

0 commit comments

Comments
 (0)