Expokit
AboutDocsComponentsChartsBlocksPricing
Sign inGet started
All components

Inputs

CalendarNewDate PickerNewFloating InputOTP InputPassword InputSearch BarSelectSliderStepperTag InputTextareaTime PickerNew

Selection

CheckboxChip GroupColor SwatchProRadio GroupRatingProSegmented ControlSwitch

Buttons

ButtonFloating Action ButtonProLike ButtonProMorph ButtonProShimmer ButtonProSlide to ConfirmProStateful Button

Feedback

AlertProgressSkeletonSpinnerToast

Overlays

Action SheetNewBottom SheetDialogDrawerNew

Display

AccordionAvatarBadgeCardCarouselProList ItemNewPagination DotsNewSwipeable RowProTimelineNew

Navigation

Bottom NavNewProgress StepsNewTabs

Motion

Number TickerProText Animation
Password Input
Sponsor ExpoKit

Free stays free. Tip once — keep shipping.

Support
About
Docs
IntroductionWhat ExpoKit is and how it worksInstallationAdd ExpoKit to an Expo appThemingColours, radius and dark modeCLIinit, add, login and updateAI assistants (MCP)Cursor, Claude, Antigravity and moreRun on your phoneTry components in Expo GoChangelogWhat shipped recently
npx @expokit/cli@latest initQuick start
Components
Liquid Glass8 iOS 26 glass pieces3DNew14 tilt, flip and stack
Inputs 12
  • CalendarNew
  • Date PickerNew
  • Floating Input
  • OTP Input
  • Password Input
  • +7 more
Selection 7
  • Checkbox
  • Chip Group
  • Color SwatchPro
  • Radio Group
  • RatingPro
  • +2 more
Buttons 7
  • Button
  • Floating Action ButtonPro
  • Like ButtonPro
  • Morph ButtonPro
  • Shimmer ButtonPro
  • +2 more
Feedback 5
  • Alert
  • Progress
  • Skeleton
  • Spinner
  • Toast
Overlays 4
  • Action SheetNew
  • Bottom Sheet
  • Dialog
  • DrawerNew
Display 9
  • Accordion
  • Avatar
  • Badge
  • Card
  • CarouselPro
  • +4 more
Navigation 3
  • Bottom NavNew
  • Progress StepsNew
  • Tabs
Motion 2
  • Number TickerPro
  • Text Animation
70 components · 347 live variantsAll components
Charts

Cartesian

  • Area Chart
  • Bar Chart
  • Line Chart
  • Combo Chart

Circular

  • Pie & Donut
  • Radial Chart
  • Radar Chart
  • Polar AreaPro

3D & Pixel

  • Isometric BarsPro
  • Pixel BarsPro

Specialty

  • TreemapPro
  • WaterfallPro
  • BulletPro
  • TimelinePro
  • Gauge
  • Candlestick
  • Heatmap
  • Funnel

Classic

  • Sparkline
  • Progress Rings
  • MultiLine (classic)
  • StackedBar (classic)
  • Ranking Bars (classic)

Guides

  • Usage & config
  • Patterns & effects
  • Show / hide anything
  • Chart theming
  • Interactions
23 charts, built on the open-source expo-charts engineAll charts
Blocks

Screens

ChatProAn AI assistant chat screenCheckoutProA premium checkoutDashboardProA finance / analytics home screenInboxProA messaging inboxOnboardingProA minimal swipeable welcome flowProfileProAccount profile for any appSearch ResultsProA clean, monochrome search screenSettingsProAn iOS-style grouped settings screenSign InProOne configurable sign-in flow modelled on top mobile apps
9 complete screens — drop one in, wire your dataSee them on devices All blocks
Pricing
DocsComponentsInputsPassword Input

Password Input

Scan with Expo Go

Password Input with all its variants, native in Expo Go. No account needed.

No Expo Go? Open in your browser

An eye toggle that twists between states, and a strength meter that springs full.

<PasswordInput />5 variantsiOS · Android · Expo Go
9:41

Sunday 18 October

9:41

Installation#

One command copies the source into your app — you own the code and can change anything.

Usage#

Colours, radius and light / dark come from useKit(). Wrap your app in <KitProvider accent="#6366f1" radius={14}> to re-skin every component at once.

Props#

PropTypeDefault
strength

Show the animated strength meter.

booleantrue
score

Custom strength scorer.

(pw) => 0–4built-in
requirements

Checklist under the field; true uses DEFAULT_PASSWORD_REQUIREMENTS (8+ chars, upper & lower, number, symbol).

{ label, test }[] | boolean—
confirmOf

Confirm-password mode: shows match / mismatch once the user types.

string—
matchMessage / mismatchMessage

Copy for confirm mode.

string"Passwords match" / "Passwords don't match"
floatingLabel

Passed to FloatingInput: set to false to put the label above the field (no notch) and show placeholder inside.

booleantrue
iconColor

Eye / eye-off toggle and clear button colour.

stringtheme mutedForeground
fieldStyle / inputStyle / labelStyle / messageStyle / counterStyle

Style slots passed straight to FloatingInput — see Floating Input.

StyleProp—
…FloatingInputProps

Every Floating Input prop (error, success, size, variant, …).

Behaviour & accessibility#

Touch
The eye button has a 44pt hit area and toggles secureTextEntry. With confirmOf, a match or mismatch message appears as soon as the typing diverges.
Motion
Eye and eye-off cross-fade with a spring twist. The four meter segments fill left to right in a stagger, and checklist ticks pop in as rules are met.
Keyboard
Auto-capitalisation and autocorrect are turned off for the field.
Screen readers
The toggle is a button labelled 'Show password' or 'Hide password'. The meter reads 'Password strength: Strong' and each requirement reads as done or not yet.

Source#

PreviousOTP InputNextSearch Bar

Did you like the content?

Coding agents

Chat apps

Other

9:41

Create Password

Password Input

Manual installation
  1. Install dependencies

    terminal
    npx expo install react-native-svg
  2. Copy the files

    Create each file at the same path and paste its code. @/ points at your src/ folder (the default in new Expo apps). Skip any file you already have, like lib/kit.tsx.

    • src/components/ui/password-input.tsx272 lines
    • src/lib/kit.tsx220 lines
    • src/components/ui/floating-input.tsx529 lines

    Want to read it first? Every file is in Source below.

  3. Use it

    App.tsx
    import { PasswordInput } from "@/components/ui/password-input";

    See Usage for a full example.

Example.tsx
import { useState } from 'react';import { PasswordInput } from '@/components/ui/password-input'; const [password, setPassword] = useState('');<PasswordInput label="New password" value={password} onChangeText={setPassword} requirements />

Every variant above has a complete, runnable example in its Code tab.

src/components/ui/password-input.tsx
import React, { forwardRef, useEffect, useMemo, useRef, useState, type ComponentRef } from 'react';import { Animated, Pressable, StyleSheet, Text, View, type TextInput } from 'react-native';import Svg, { Circle, Path } from 'react-native-svg'; import { FloatingInput, type FloatingInputProps } from '@/components/ui/floating-input';import { alpha, motion, useKit } from '@/lib/kit'; export interface PasswordRequirement {  /** Shown in the checklist, e.g. 'At least 8 characters'. */  label: string;  test: (password: string) => boolean;} /** A sensible default checklist: length, mixed case, a number, a symbol. */export const DEFAULT_PASSWORD_REQUIREMENTS: PasswordRequirement[] = [  { label: '8+ characters', test: (v) => v.length >= 8 },  { label: 'Upper & lower case', test: (v) => /[a-z]/.test(v) && /[A-Z]/.test(v) },  { label: 'A number', test: (v) => /\d/.test(v) },  { label: 'A symbol', test: (v) => /[^A-Za-z0-9\s]/.test(v) },]; export interface PasswordInputProps extends Omit<FloatingInputProps, 'secureTextEntry' | 'right'> {  /** Show the 4-segment strength meter under the field. */  strength?: boolean;  /** Replace the built-in scorer (return 0–4). */  score?: (password: string) => number;  /** Checklist under the field; each row ticks itself off when met. `true` uses DEFAULT_PASSWORD_REQUIREMENTS. */  requirements?: PasswordRequirement[] | boolean;  /** Makes this a "confirm password" field: shows a match / mismatch message once the user types. */  confirmOf?: string;  /** Message when `confirmOf` matches (default 'Passwords match'). */  matchMessage?: string;  /** Message when `confirmOf` doesn't match (default "Passwords don't match"). */  mismatchMessage?: string;} const LEVEL_LABELS = ['Too short', 'Weak', 'Okay', 'Strong', 'Very strong'];const ICON = { sm: 18, md: 22, lg: 24 }; export function scorePassword(pw: string) {  if (pw.length < 6) return 0;  let s = 1;  if (pw.length >= 10) s++;  if (/[A-Z]/.test(pw) && /[a-z]/.test(pw)) s++;  if (/\d/.test(pw) && /[^A-Za-z0-9]/.test(pw)) s++;  return Math.min(s, 4);} /** * Floating-label password field with an eye / eye-off toggle that cross-fades, a spring-filled * strength meter, an optional requirements checklist and a confirm-password mode. */export const PasswordInput = forwardRef<ComponentRef<typeof TextInput>, PasswordInputProps>(function PasswordInput(  {    strength = true,    score = scorePassword,    requirements,    confirmOf,    matchMessage = 'Passwords match',    mismatchMessage = "Passwords don't match",    value,    defaultValue,    onChangeText,    error,    success,    size = 'md',    iconColor: iconColorProp,    ...props  },  ref) {  const { colors } = useKit();  const [hidden, setHidden] = useState(true);  const [inner, setInner] = useState(defaultValue ?? '');  const text = value ?? inner;  const confirming = confirmOf !== undefined;  const level = useMemo(() => score(text), [score, text]);  const rules = requirements === true ? DEFAULT_PASSWORD_REQUIREMENTS : requirements || undefined;   // Confirm mode: complain as soon as the typed prefix diverges, or once it's long enough to compare.  const matches = confirming && text.length > 0 && text === confirmOf;  const mismatch =    confirming && text.length > 0 && !matches && (text.length >= confirmOf.length || !confirmOf.startsWith(text));   const eye = useRef(new Animated.Value(hidden ? 0 : 1)).current; // native: 1 = visible  useEffect(() => {    Animated.spring(eye, { toValue: hidden ? 0 : 1, useNativeDriver: true, ...motion.snappy }).start();  }, [hidden, eye]);   const iconColor = iconColorProp ?? colors.mutedForeground;  const iconSize = ICON[size];  const fade = (on: 0 | 1) => ({    opacity: eye.interpolate({ inputRange: [0, 1], outputRange: on ? [0, 1] : [1, 0], extrapolate: 'clamp' }),    transform: [      { scale: eye.interpolate({ inputRange: [0, 1], outputRange: on ? [0.6, 1] : [1, 0.6] }) },      { rotate: eye.interpolate({ inputRange: [0, 1], outputRange: on ? ['-45deg', '0deg'] : ['0deg', '45deg'] }) },    ],  });   return (    <View>      <FloatingInput        ref={ref}        {...props}        size={size}        iconColor={iconColorProp}        value={text}        onChangeText={(next) => {          if (value === undefined) setInner(next);          onChangeText?.(next);        }}        error={error ?? (mismatch ? mismatchMessage : undefined)}        success={success ?? (matches ? matchMessage : undefined)}        secureTextEntry={hidden}        autoCapitalize="none"        autoCorrect={false}        right={          <Pressable            hitSlop={(44 - iconSize) / 2}            onPress={() => setHidden((h) => !h)}            accessibilityRole="button"            accessibilityLabel={hidden ? 'Show password' : 'Hide password'}            accessibilityState={{ checked: !hidden }}            style={{ width: iconSize, height: iconSize }}          >            <Animated.View style={[StyleSheet.absoluteFill, fade(1)]}>              <Svg width={iconSize} height={iconSize} viewBox="0 0 24 24" fill="none">                <Path d={EYE} stroke={iconColor} strokeWidth={1.8} strokeLinecap="round" strokeLinejoin="round" />                <Circle cx={12} cy={12} r={3} stroke={iconColor} strokeWidth={1.8} />              </Svg>            </Animated.View>            <Animated.View style={[StyleSheet.absoluteFill, fade(0)]}>              <Svg width={iconSize} height={iconSize} viewBox="0 0 24 24" fill="none">                <Path d={EYE_OFF} stroke={iconColor} strokeWidth={1.8} strokeLinecap="round" strokeLinejoin="round" />              </Svg>            </Animated.View>          </Pressable>        }      />      {strength && !confirming && text.length > 0 ? <Meter level={level} /> : null}      {rules && !confirming ? <Checklist rules={rules} value={text} /> : null}    </View>  );}); const EYE = 'M2.06 12.35a1 1 0 0 1 0-.7 10.75 10.75 0 0 1 19.88 0 1 1 0 0 1 0 .7 10.75 10.75 0 0 1-19.88 0';const EYE_OFF =  'M10.73 5.08A10.43 10.43 0 0 1 12 5c4.65 0 8.6 2.95 9.94 6.65a1 1 0 0 1 0 .7 10.75 10.75 0 0 1-1.44 2.49' +  'M14.08 14.16a3 3 0 0 1-4.24-4.24' +  'M17.48 17.5A10.75 10.75 0 0 1 2.06 12.35a1 1 0 0 1 0-.7 10.75 10.75 0 0 1 4.45-5.14' +  'M2 2l20 20'; /** Four segments that spring-fill left to right; the colour slides with the score. */function Meter({ level }: { level: number }) {  const { colors, dark, font } = useKit();  const palette = [colors.mutedForeground, colors.danger, colors.warning, colors.success, colors.success];  const fills = useRef([0, 1, 2, 3].map(() => new Animated.Value(0))).current; // native: scaleX  const tone = useRef(new Animated.Value(level)).current; // JS: colour  const enter = useRef(new Animated.Value(0)).current; // native   useEffect(() => {    Animated.spring(enter, { toValue: 1, useNativeDriver: true, ...motion.snappy }).start();  }, [enter]);   useEffect(() => {    Animated.stagger(      50,      fills.map((a, i) => Animated.spring(a, { toValue: i < level ? 1 : 0, useNativeDriver: true, ...motion.snappy }))    ).start();    Animated.timing(tone, { toValue: level, duration: 220, useNativeDriver: false }).start();  }, [level, fills, tone]);   const color = tone.interpolate({ inputRange: [0, 1, 2, 3, 4], outputRange: palette });  const track = dark ? colors.muted : colors.border;   return (    <Animated.View      style={[        styles.meter,        {          opacity: enter.interpolate({ inputRange: [0, 1], outputRange: [0, 1], extrapolate: 'clamp' }),          transform: [{ translateY: enter.interpolate({ inputRange: [0, 1], outputRange: [4, 0] }) }],        },      ]}      accessible      accessibilityLabel={`Password strength: ${LEVEL_LABELS[level]}`}    >      <View style={styles.bars}>        {fills.map((a, i) => (          <View key={i} style={[styles.track, { backgroundColor: track }]}>            <Animated.View              style={[                styles.fill,                { transform: [{ scaleX: a.interpolate({ inputRange: [0, 1], outputRange: [0, 1], extrapolate: 'clamp' }) }] },              ]}            >              <Animated.View style={[StyleSheet.absoluteFill, { backgroundColor: color }]} />            </Animated.View>          </View>        ))}      </View>      <Text style={[styles.level, font.family ? { fontFamily: font.family } : null, { color: palette[level] }]}>{LEVEL_LABELS[level]}</Text>    </Animated.View>  );} function Checklist({ rules, value }: { rules: PasswordRequirement[]; value: string }) {  return (    <View style={styles.checklist} accessibilityRole="list">      {rules.map((r) => (        <Requirement key={r.label} label={r.label} met={r.test(value)} />      ))}    </View>  );} function Requirement({ label, met }: { label: string; met: boolean }) {  const { colors, dark, font } = useKit();  const tick = useRef(new Animated.Value(met ? 1 : 0)).current; // native  useEffect(() => {    Animated.spring(tick, { toValue: met ? 1 : 0, useNativeDriver: true, ...motion.bouncy }).start();  }, [met, tick]);   return (    <View      style={styles.rule}      accessible      accessibilityLabel={`${label}: ${met ? 'done' : 'not yet'}`}      accessibilityState={{ checked: met }}    >      <View style={styles.ruleIcon}>        <Svg width={16} height={16} viewBox="0 0 24 24" fill="none">          <Circle cx={12} cy={12} r={9.5} stroke={alpha(colors.mutedForeground, 0.5)} strokeWidth={1.8} />        </Svg>        <Animated.View          style={[            StyleSheet.absoluteFill,            {              opacity: tick.interpolate({ inputRange: [0, 1], outputRange: [0, 1], extrapolate: 'clamp' }),              transform: [{ scale: tick.interpolate({ inputRange: [0, 1], outputRange: [0.2, 1] }) }],            },          ]}        >          <Svg width={16} height={16} viewBox="0 0 24 24" fill="none">            <Circle cx={12} cy={12} r={11} fill={colors.success} />            <Path              d="M7.5 12.5l3 3 6-6.5"              stroke={dark ? '#0b0b12' : '#ffffff'}              strokeWidth={2.4}              strokeLinecap="round"              strokeLinejoin="round"            />          </Svg>        </Animated.View>      </View>      <Text style={[styles.ruleText, font.family ? { fontFamily: font.family } : null, { color: met ? colors.foreground : colors.mutedForeground }]}>{label}</Text>    </View>  );} const styles = StyleSheet.create({  meter: { flexDirection: 'row', alignItems: 'center', gap: 10, marginTop: 10, marginHorizontal: 4 },  bars: { flex: 1, flexDirection: 'row', gap: 6 },  track: { flex: 1, height: 5, borderRadius: 3, overflow: 'hidden' },  fill: { height: '100%', borderRadius: 3, overflow: 'hidden', transformOrigin: 'left center' },  level: { fontSize: 12.5, fontWeight: '400', minWidth: 72, textAlign: 'right' },  checklist: { flexDirection: 'row', flexWrap: 'wrap', rowGap: 8, marginTop: 12, marginHorizontal: 4 },  rule: { width: '50%', flexDirection: 'row', alignItems: 'center', gap: 7, paddingRight: 6 },  ruleIcon: { width: 16, height: 16 },  ruleText: { flexShrink: 1, fontSize: 12.5, lineHeight: 17 },});