Amplify UI

Customization

Override and customize your Authenticator.

The Authenticator has several "slots" that you can customize to add messaging & functionality to meet your app's needs.

The Authenticator itself has the following optional slots which are rendered on each subcomponent:

  • Container - Wraps the Authenticator. Can be overridden by extending Authenticator.Container or with a View component
  • Footer - renders below subcomponent content, no default provided
  • Header - renders above subcomponent content, no default provided

The default Container component contains keyboard aware scroll behavior.

import React from 'react';
import { Button, StyleSheet, Text, View } from 'react-native';

import { Amplify } from 'aws-amplify';
import {
  Authenticator,
  useAuthenticator,
  useTheme,
} from '@aws-amplify/ui-react-native';

import awsconfig from './aws-exports';
Amplify.configure(awsconfig);

const MyAppHeader = () => {
  const {
    tokens: { space, fontSizes },
  } = useTheme();
  return (
    <View>
      <Text style={{ fontSize: fontSizes.xxxl, padding: space.xl }}>
        My Header
      </Text>
    </View>
  );
};

function SignOutButton() {
  const { signOut } = useAuthenticator();
  return <Button onPress={signOut} title="Sign Out" />;
}

function App() {
  const {
    tokens: { colors },
  } = useTheme();

  return (
    <Authenticator.Provider>
      <Authenticator
        // will wrap every subcomponent
        Container={(props) => (
          // reuse default `Container` and apply custom background
          <Authenticator.Container
            {...props}
            style={{ backgroundColor: colors.pink[20] }}
          />
        )}
        // will render on every subcomponent
        Header={MyAppHeader}
      >
        <View style={style.container}>
          <SignOutButton />
        </View>
      </Authenticator>
    </Authenticator.Provider>
  );
}

const style = StyleSheet.create({
  container: { flex: 1, alignItems: 'center', justifyContent: 'center' },
});

export default App;

Addtionally, the subcomponents themselves have Header and Footer slots specific to a subcomponent:

  • Footer - renders below subcomponent buttons, above top level Footer, no default provided
  • Header - renders above subcomponent content, below top level Header, default renders subcomponent title
import React from 'react';
import { Button, StyleSheet, Text, View } from 'react-native';

import { Amplify } from 'aws-amplify';
import { Authenticator, useAuthenticator } from '@aws-amplify/ui-react-native';

import awsconfig from './aws-exports';
Amplify.configure(awsconfig);

const MySignInFooter = () => <Text>My Footer</Text>;

function SignOutButton() {
  const { signOut } = useAuthenticator();
  return <Button onPress={signOut} title="Sign Out" />;
}

function App() {
  return (
    <Authenticator.Provider>
      <Authenticator
        components={{
          SignUp: (props) => (
            // will render only on the SignIn subcomponent
            <Authenticator.SignUp {...props} Footer={MySignInFooter} />
          ),
        }}
      >
        <View style={style.container}>
          <SignOutButton />
        </View>
      </Authenticator>
    </Authenticator.Provider>
  );
}

const style = StyleSheet.create({
  container: { flex: 1, alignItems: 'center', justifyContent: 'center' },
});

export default App;

Subcomponent Override Slots

The Authenticator subcomponents can be overridden allowing for advanced use cases:

import React from 'react';
import { Button, StyleSheet, Text, View } from 'react-native';

import { Amplify } from 'aws-amplify';
import { Authenticator, useAuthenticator } from '@aws-amplify/ui-react-native';

import awsconfig from './aws-exports';
Amplify.configure(awsconfig);

function SignOutButton() {
  const { signOut } = useAuthenticator();
  return <Button onPress={signOut} title="Sign Out" />;
}

const MySignIn = () => {
  return (
    <View>
      <Text>My Sign In</Text>
    </View>
  );
};

function App() {
  return (
    <Authenticator.Provider>
      <Authenticator
        // render override SignIn subcomponent
        components={{ SignIn: MySignIn }}
      >
        <View style={style.container}>
          <SignOutButton />
        </View>
      </Authenticator>
    </Authenticator.Provider>
  );
}

const style = StyleSheet.create({
  container: { flex: 1, alignItems: 'center', justifyContent: 'center' },
});

export default App;

Override components with third-party component library

The following example shows how to override each component in the Authenticator to produce a distinct look and feel using a third-party component library.

/**
 * Example.tsx
 */

import React from 'react';

import { useColorScheme } from 'react-native';
import {
  MD3LightTheme as LightTheme,
  MD3DarkTheme as DarkTheme,
  PaperProvider,
} from 'react-native-paper';
import {
  Authenticator,
  AuthenticatorProps,
} from '@aws-amplify/ui-react-native';
import { Amplify } from 'aws-amplify';
import outputs from '@aws-amplify/ui-environments/auth/gen2/auth-with-federated-sign-in-react-native/amplify_outputs.json';

import { ConfirmResetPassword } from './ConfirmResetPassword';
import { ConfirmSignIn } from './ConfirmSignIn';
import { ConfirmSignUp } from './ConfirmSignUp';
import { ConfirmVerifyUser } from './ConfirmVerifyUser';
import { ForceNewPassword } from './ForceNewPassword';
import { ForgotPassword } from './ForgotPassword';
import { SelectMfaType } from './SelectMfaType';
import { SetupEmail } from './SetupEmail';
import { SetupTotp } from './SetupTotp';
import { SignIn } from './SignIn';
import { SignUp } from './SignUp';
import { VerifyUser } from './VerifyUser';
import { Container, SignOutButton } from './components';

Amplify.configure(outputs);

const components: AuthenticatorProps['components'] = {
  ConfirmResetPassword,
  ConfirmSignIn,
  ConfirmSignUp,
  ConfirmVerifyUser,
  ForceNewPassword,
  ForgotPassword,
  SelectMfaType,
  SetupEmail,
  SetupTotp,
  SignIn,
  SignUp,
  VerifyUser,
};

function App() {
  const isDarkMode = useColorScheme() === 'dark';

  return (
    <PaperProvider theme={isDarkMode ? DarkTheme : LightTheme}>
      <Authenticator.Provider>
        <Authenticator Container={Container} components={components}>
          <SignOutButton />
        </Authenticator>
      </Authenticator.Provider>
    </PaperProvider>
  );
}

export default App;
/**
 * ConfirmResetPassword.tsx
 */

import React from 'react';

import { useForm } from 'react-hook-form';
import { ConfirmResetPasswordProps } from '@aws-amplify/ui-react-native';

import {
  ErrorMessage,
  LinkButton,
  LinksContainer,
  SubmitButton,
  TextField,
  ViewHeader,
  ViewContainer,
  ViewSection,
} from './components';

export function ConfirmResetPassword({
  error: errorMessage,
  fields,
  handleSubmit,
  isPending,
  resendCode,
}: ConfirmResetPasswordProps): React.JSX.Element {
  const {
    control,
    formState: { errors, isValid },
    getValues,
  } = useForm({ mode: 'onTouched' });

  return (
    <ViewContainer>
      <ViewHeader>Confirm Reset Password</ViewHeader>

      <ViewSection>
        {fields.map(({ name, label, ...field }) => (
          <TextField
            {...field}
            control={control}
            error={errors?.[name]?.message as string}
            key={name}
            label={label}
            name={name}
            rules={{ required: `${label} is required` }}
          />
        ))}
      </ViewSection>

      <SubmitButton
        disabled={!isValid}
        loading={isPending}
        onPress={() => {
          handleSubmit(getValues());
        }}
      >
        Submit
      </SubmitButton>

      <ErrorMessage>{errorMessage}</ErrorMessage>

      <LinksContainer>
        <LinkButton onPress={resendCode}>Resend Code</LinkButton>
      </LinksContainer>
    </ViewContainer>
  );
}
/**
 * ConfirmSignIn.tsx
 */

import React from 'react';

import { useForm } from 'react-hook-form';
import { ConfirmSignInProps } from '@aws-amplify/ui-react-native';

import {
  ErrorMessage,
  LinkButton,
  LinksContainer,
  SubmitButton,
  TextField,
  ViewHeader,
  ViewContainer,
  ViewSection,
} from './components';

export function ConfirmSignIn({
  error: errorMessage,
  fields,
  handleSubmit,
  isPending,
  toSignIn,
}: ConfirmSignInProps): React.JSX.Element {
  const {
    control,
    formState: { errors, isValid },
    getValues,
  } = useForm({ mode: 'onTouched' });

  return (
    <ViewContainer>
      <ViewHeader>Confirm Sign In</ViewHeader>

      <ViewSection>
        {fields.map(({ name, label, ...field }) => (
          <TextField
            {...field}
            control={control}
            error={errors?.[name]?.message as string}
            key={name}
            label={label}
            name={name}
            rules={{ required: `${label} is required` }}
          />
        ))}
      </ViewSection>

      <SubmitButton
        disabled={!isValid}
        loading={isPending}
        onPress={() => {
          handleSubmit(getValues());
        }}
      >
        Submit
      </SubmitButton>

      <ErrorMessage>{errorMessage}</ErrorMessage>

      <LinksContainer>
        <LinkButton onPress={toSignIn}>Back To Sign In</LinkButton>
      </LinksContainer>
    </ViewContainer>
  );
}
/**
 * ConfirmVerifyUser.tsx
 */

import React from 'react';

import { useForm } from 'react-hook-form';
import { ConfirmVerifyUserProps } from '@aws-amplify/ui-react-native';

import {
  ErrorMessage,
  LinkButton,
  LinksContainer,
  SubmitButton,
  TextField,
  ViewHeader,
  ViewContainer,
  ViewSection,
} from './components';

export function ConfirmVerifyUser({
  error: errorMessage,
  fields,
  handleSubmit,
  isPending,
  skipVerification,
}: ConfirmVerifyUserProps): React.JSX.Element {
  const {
    control,
    formState: { errors, isValid },
    getValues,
  } = useForm({ mode: 'onTouched' });

  return (
    <ViewContainer>
      <ViewHeader>Confirm Verify User</ViewHeader>

      <ViewSection>
        {fields.map(({ name, label, ...field }) => (
          <TextField
            {...field}
            control={control}
            error={errors?.[name]?.message as string}
            key={name}
            label={label}
            name={name}
            rules={{ required: `${label} is required` }}
          />
        ))}
      </ViewSection>

      <SubmitButton
        disabled={!isValid}
        loading={isPending}
        onPress={() => {
          handleSubmit(getValues());
        }}
      >
        Submit
      </SubmitButton>

      <ErrorMessage>{errorMessage}</ErrorMessage>

      <LinksContainer>
        <LinkButton onPress={skipVerification}>Skip</LinkButton>
      </LinksContainer>
    </ViewContainer>
  );
}
/**
 * ForceNewPassword.tsx
 */

import React from 'react';

import { useForm } from 'react-hook-form';
import { ForceNewPasswordProps } from '@aws-amplify/ui-react-native';

import {
  ErrorMessage,
  LinkButton,
  LinksContainer,
  SubmitButton,
  TextField,
  ViewHeader,
  ViewContainer,
  ViewSection,
} from './components';

export function ForceNewPassword({
  error: errorMessage,
  fields,
  handleSubmit,
  isPending,
  toSignIn,
}: ForceNewPasswordProps): React.JSX.Element {
  const {
    control,
    formState: { errors, isValid },
    getValues,
  } = useForm({ mode: 'onTouched' });

  return (
    <ViewContainer>
      <ViewHeader>Change Password</ViewHeader>

      <ViewSection>
        {fields.map(({ name, label, ...field }) => (
          <TextField
            {...field}
            control={control}
            error={errors?.[name]?.message as string}
            key={name}
            label={label}
            name={name}
            rules={{ required: `${label} is required` }}
          />
        ))}
      </ViewSection>

      <SubmitButton
        disabled={!isValid}
        loading={isPending}
        onPress={() => {
          handleSubmit(getValues());
        }}
      >
        Submit
      </SubmitButton>

      <ErrorMessage>{errorMessage}</ErrorMessage>

      <LinksContainer>
        <LinkButton onPress={toSignIn}>Back To Sign In</LinkButton>
      </LinksContainer>
    </ViewContainer>
  );
}
/**
 * ForgotPassword.tsx
 */

import React from 'react';

import { useForm } from 'react-hook-form';
import { ForgotPasswordProps } from '@aws-amplify/ui-react-native';

import {
  ErrorMessage,
  LinkButton,
  LinksContainer,
  SubmitButton,
  TextField,
  ViewHeader,
  ViewContainer,
  ViewSection,
} from './components';

export function ForgotPassword({
  error: errorMessage,
  fields,
  handleSubmit,
  isPending,

  toSignIn,
}: ForgotPasswordProps): React.JSX.Element {
  const {
    control,
    formState: { errors, isValid },
    getValues,
  } = useForm({ mode: 'onTouched' });

  return (
    <ViewContainer>
      <ViewHeader>Forgot Password</ViewHeader>

      <ViewSection>
        {fields.map(({ name, label, ...field }) => (
          <TextField
            {...field}
            control={control}
            error={errors?.[name]?.message as string}
            key={name}
            label={label}
            name={name}
            rules={{ required: `${label} is required` }}
          />
        ))}
      </ViewSection>

      <SubmitButton
        disabled={!isValid}
        loading={isPending}
        onPress={() => {
          handleSubmit(getValues());
        }}
      >
        Submit
      </SubmitButton>

      <ErrorMessage>{errorMessage}</ErrorMessage>

      <LinksContainer>
        <LinkButton onPress={toSignIn}>Back To Sign In</LinkButton>
      </LinksContainer>
    </ViewContainer>
  );
}
/**
 * SelectMfaType.tsx
 */

import React from 'react';

import { Controller, useForm } from 'react-hook-form';
import { SelectMfaTypeProps } from '@aws-amplify/ui-react-native';

import {
  ErrorMessage,
  LinkButton,
  LinksContainer,
  SubmitButton,
  ViewHeader,
  ViewContainer,
  ViewSection,
} from './components';
import { RadioButton } from 'react-native-paper';

export function SelectMfaType({
  error: errorMessage,
  fields,
  handleSubmit,
  isPending,
  toSignIn,
}: SelectMfaTypeProps): React.JSX.Element {
  const {
    control,
    formState: { isValid },
    getValues,
  } = useForm({ mode: 'onTouched' });

  return (
    <ViewContainer>
      <ViewHeader>Select MFA Type</ViewHeader>

      <ViewSection>
        <Controller
          control={control}
          name="mfa_type"
          render={({ field: { onChange, value } }) => (
            <RadioButton.Group onValueChange={onChange} value={value}>
              {fields.map((field) => (
                <RadioButton.Item
                  key={field.value}
                  label={field.label!}
                  value={field.value!}
                />
              ))}
            </RadioButton.Group>
          )}
        />
      </ViewSection>

      <SubmitButton
        disabled={!isValid}
        loading={isPending}
        onPress={() => {
          handleSubmit(getValues());
        }}
      >
        Submit
      </SubmitButton>

      <ErrorMessage>{errorMessage}</ErrorMessage>

      <LinksContainer>
        <LinkButton onPress={toSignIn}>Back To Sign In</LinkButton>
      </LinksContainer>
    </ViewContainer>
  );
}
/**
 * SetupEmail.tsx
 */

import React from 'react';

import { useForm } from 'react-hook-form';
import { SetupEmailProps } from '@aws-amplify/ui-react-native';

import {
  ErrorMessage,
  LinkButton,
  LinksContainer,
  SubmitButton,
  TextField,
  ViewHeader,
  ViewContainer,
  ViewSection,
} from './components';

export function SetupEmail({
  error: errorMessage,
  fields,
  handleSubmit,
  isPending,
  toSignIn,
}: SetupEmailProps): React.JSX.Element {
  const {
    control,
    formState: { errors, isValid },
    getValues,
  } = useForm({ mode: 'onTouched' });

  return (
    <ViewContainer>
      <ViewHeader>Confirm Sign In</ViewHeader>

      <ViewSection>
        {fields.map(({ name, label, ...field }) => (
          <TextField
            {...field}
            control={control}
            error={errors?.[name]?.message as string}
            key={name}
            label={label}
            name={name}
            rules={{ required: `${label} is required` }}
          />
        ))}
      </ViewSection>

      <SubmitButton
        disabled={!isValid}
        loading={isPending}
        onPress={() => {
          handleSubmit(getValues());
        }}
      >
        Submit
      </SubmitButton>

      <ErrorMessage>{errorMessage}</ErrorMessage>

      <LinksContainer>
        <LinkButton onPress={toSignIn}>Back To Sign In</LinkButton>
      </LinksContainer>
    </ViewContainer>
  );
}
/**
 * SetupTotp.tsx
 */

import React from 'react';

import { useForm } from 'react-hook-form';
import { SetupTotpProps } from '@aws-amplify/ui-react-native';

import {
  ErrorMessage,
  LinkButton,
  LinksContainer,
  SubmitButton,
  TextField,
  ViewHeader,
  ViewContainer,
  ViewSection,
  ViewDivider,
} from './components';
import { Text } from 'react-native-paper';

export function SetupTotp({
  error: errorMessage,
  fields,
  handleSubmit,
  isPending,
  toSignIn,
  totpSecretCode,
}: SetupTotpProps): React.JSX.Element {
  const {
    control,
    formState: { errors, isValid },
    getValues,
  } = useForm({ mode: 'onTouched' });

  return (
    <ViewContainer>
      <ViewHeader>Setup TOTP</ViewHeader>

      <ViewSection>
        <Text variant="bodyLarge">
          Copy and paste the secret key below into an authenticator app and then
          enter the code in the text field below.
        </Text>
        <ViewDivider />
        <Text selectable variant="bodyLarge">
          {totpSecretCode}
        </Text>
      </ViewSection>

      <ViewSection>
        {fields.map(({ name, label, ...field }) => (
          <TextField
            {...field}
            control={control}
            error={errors?.[name]?.message as string}
            key={name}
            label={label}
            name={name}
            rules={{ required: `${label} is required` }}
          />
        ))}
      </ViewSection>

      <SubmitButton
        disabled={!isValid}
        loading={isPending}
        onPress={() => {
          handleSubmit(getValues());
        }}
      >
        Submit
      </SubmitButton>

      <ErrorMessage>{errorMessage}</ErrorMessage>

      <LinksContainer>
        <LinkButton onPress={toSignIn}>Back To Sign In</LinkButton>
      </LinksContainer>
    </ViewContainer>
  );
}
/**
 * SignIn.tsx
 */

import React from 'react';

import { useForm } from 'react-hook-form';
import { SignInProps } from '@aws-amplify/ui-react-native';

import {
  ErrorMessage,
  LinkButton,
  LinksContainer,
  ProviderButton,
  SubmitButton,
  TextField,
  ViewHeader,
  ViewContainer,
  ViewSection,
  ViewDivider,
} from './components';

function capitalize<T extends string>([first, ...rest]: T): Capitalize<T> {
  return [first && first.toUpperCase(), rest.join('').toLowerCase()]
    .filter(Boolean)
    .join('') as Capitalize<T>;
}

export function SignIn({
  error: errorMessage,
  fields,
  handleSubmit,
  isPending,
  socialProviders,
  toFederatedSignIn,
  toForgotPassword,
  toSignUp,
}: SignInProps): React.JSX.Element {
  const {
    control,
    formState: { errors, isValid },
    getValues,
  } = useForm({ mode: 'onTouched' });

  return (
    <ViewContainer>
      <ViewHeader>Sign In</ViewHeader>

      <ViewSection>
        {socialProviders?.map((name) => {
          const provider = capitalize(name);
          return (
            <ProviderButton
              icon={name}
              key={provider}
              onPress={() => toFederatedSignIn({ provider })}
            >
              Sign in with {provider}
            </ProviderButton>
          );
        }) ?? null}
      </ViewSection>

      <ViewDivider />

      <ViewSection>
        {fields.map(({ name, label, ...field }) => (
          <TextField
            {...field}
            control={control}
            error={errors?.[name]?.message as string}
            key={name}
            label={label}
            name={name}
            rules={{ required: `${label} is required` }}
          />
        ))}
      </ViewSection>

      <SubmitButton
        disabled={!isValid}
        loading={isPending}
        onPress={() => {
          handleSubmit(getValues());
        }}
      >
        Submit
      </SubmitButton>

      <ErrorMessage>{errorMessage}</ErrorMessage>

      <LinksContainer>
        <LinkButton onPress={toSignUp}>Sign Up</LinkButton>
        <LinkButton onPress={toForgotPassword}>Forgot Password?</LinkButton>
      </LinksContainer>
    </ViewContainer>
  );
}
/**
 * SignUp.tsx
 */

import React from 'react';

import { useForm } from 'react-hook-form';
import { SignUpProps } from '@aws-amplify/ui-react-native';

import {
  ErrorMessage,
  LinkButton,
  LinksContainer,
  ProviderButton,
  SubmitButton,
  TextField,
  ViewHeader,
  ViewContainer,
  ViewSection,
  ViewDivider,
} from './components';

function capitalize<T extends string>([first, ...rest]: T): Capitalize<T> {
  return [first && first.toUpperCase(), rest.join('').toLowerCase()]
    .filter(Boolean)
    .join('') as Capitalize<T>;
}

export function SignUp({
  error: errorMessage,
  fields,
  handleSubmit,
  isPending,
  socialProviders,
  toFederatedSignIn,
  toSignIn,
}: SignUpProps): React.JSX.Element {
  const {
    control,
    formState: { errors, isValid },
    getValues,
  } = useForm({ mode: 'onTouched' });

  return (
    <ViewContainer>
      <ViewHeader>Sign Up</ViewHeader>

      <ViewSection>
        {socialProviders?.map((name) => {
          const provider = capitalize(name);
          return (
            <ProviderButton
              icon={name}
              key={provider}
              onPress={() => toFederatedSignIn({ provider })}
            >
              Sign up with {provider}
            </ProviderButton>
          );
        }) ?? null}
      </ViewSection>

      <ViewDivider />

      <ViewSection>
        {fields.map(({ name, label, ...field }) => (
          <TextField
            {...field}
            control={control}
            error={errors?.[name]?.message as string}
            key={name}
            label={label}
            name={name}
            rules={{ required: `${label} is required` }}
          />
        ))}
      </ViewSection>

      <SubmitButton
        disabled={!isValid}
        loading={isPending}
        onPress={() => {
          handleSubmit(getValues());
        }}
      >
        Submit
      </SubmitButton>

      <ErrorMessage>{errorMessage}</ErrorMessage>

      <LinksContainer>
        <LinkButton onPress={toSignIn}>Back To Sign In</LinkButton>
      </LinksContainer>
    </ViewContainer>
  );
}
/**
 * VerifyUser.tsx
 */

import React from 'react';

import { Controller, useForm } from 'react-hook-form';
import { RadioButton } from 'react-native-paper';
import { censorContactMethod, ContactMethod } from '@aws-amplify/ui';
import { VerifyUserProps } from '@aws-amplify/ui-react-native';

import {
  ErrorMessage,
  LinkButton,
  LinksContainer,
  SubmitButton,
  ViewHeader,
  ViewContainer,
  ViewSection,
} from './components';

interface AttributeMap {
  email: ContactMethod;
  phone_number: ContactMethod;
}

const attributeMap: AttributeMap = {
  email: 'Email',
  phone_number: 'Phone Number',
};

export function VerifyUser({
  error: errorMessage,
  fields,
  handleSubmit,
  isPending,
  skipVerification,
}: VerifyUserProps): React.JSX.Element {
  const {
    control,
    formState: { isValid },
    getValues,
  } = useForm({ mode: 'onTouched' });

  return (
    <ViewContainer>
      <ViewHeader>Verify User</ViewHeader>

      <ViewSection>
        <Controller
          control={control}
          name="unverifiedAttr"
          render={({ field: { onChange, value } }) => (
            <RadioButton.Group onValueChange={onChange} value={value}>
              {fields.map((field) => {
                const attributeType =
                  attributeMap[field.name as keyof AttributeMap];
                return (
                  <RadioButton.Item
                    key={field.value}
                    label={censorContactMethod(attributeType, field.value)}
                    value={field.name}
                  />
                );
              })}
            </RadioButton.Group>
          )}
        />
      </ViewSection>

      <SubmitButton
        disabled={!isValid}
        loading={isPending}
        onPress={() => {
          handleSubmit(getValues());
        }}
      >
        Submit
      </SubmitButton>

      <ErrorMessage>{errorMessage}</ErrorMessage>

      <LinksContainer>
        <LinkButton onPress={skipVerification}>Skip</LinkButton>
      </LinksContainer>
    </ViewContainer>
  );
}
/**
 * components.tsx
 */

import React from 'react';

import { Controller, ControllerProps } from 'react-hook-form';
import { View, ViewProps, ViewStyle } from 'react-native';
import {
  Button,
  ButtonProps,
  Headline,
  HeadlineProps,
  HelperText,
  Text,
  TextProps,
  TextInput,
  TextInputProps,
  useTheme,
  Divider,
} from 'react-native-paper';
import { Authenticator, useAuthenticator } from '@aws-amplify/ui-react-native';

const styles = {
  signOutButton: { marginVertical: 32, marginHorizontal: 16 },
  viewHeader: { marginBottom: 16 },
  viewDivider: { marginVertical: 16 },
  linksContainer: {
    flexDirection: 'row',
    justifyContent: 'space-around',
    marginTop: 16,
  } as ViewStyle,
  errorMessage: { marginTop: 16, padding: 16 },
};

export interface TextFieldProps
  extends Omit<TextInputProps, 'error' | 'cursorColor' | 'selectionColor'>,
    Pick<ControllerProps, 'control' | 'name' | 'rules'> {
  control: ControllerProps['control'];
  error?: string;
  type?: 'email' | 'default' | 'password' | 'phone';
}

export function TextField({
  control,
  error,
  name,
  type = 'default',
  rules,
  secureTextEntry,
  ...props
}: TextFieldProps) {
  return (
    <React.Fragment key={name}>
      <Controller
        control={control}
        name={name}
        render={({ field: { onChange: onChangeText, ...renderProps } }) => (
          <TextInput
            {...(props as TextInputProps)}
            {...renderProps}
            onChangeText={onChangeText}
            secureTextEntry={secureTextEntry ?? type === 'password'}
          />
        )}
        rules={rules}
      />
      <HelperText type="error" visible={!!error}>
        {error || ' '}
      </HelperText>
    </React.Fragment>
  );
}

export function SubmitButton({
  children,
  disabled,
  style,
  loading,
  ...props
}: ButtonProps) {
  const theme = useTheme();
  return (
    <Button
      {...props}
      disabled={disabled}
      loading={loading}
      style={[
        {
          backgroundColor:
            disabled || loading
              ? theme.colors.surfaceDisabled
              : theme.colors.primary,
        },

        style,
      ]}
      textColor={theme.colors.inverseOnSurface}
    >
      {children}
    </Button>
  );
}

export function ProviderButton({ children, ...props }: ButtonProps) {
  return (
    <Button {...props} mode="outlined">
      {children}
    </Button>
  );
}

export function ViewHeader({ children, style, ...props }: HeadlineProps) {
  return (
    <Headline {...props} style={[styles.viewHeader, style]}>
      {children}
    </Headline>
  );
}

export function ViewSection({ children, ...props }: ViewProps) {
  return <View {...props}>{children}</View>;
}

export function LinksContainer({ children, style, ...props }: ViewProps) {
  return (
    <View {...props} style={[styles.linksContainer, style]}>
      {children}
    </View>
  );
}

export function LinkButton(props: ButtonProps) {
  return <Button {...props} />;
}

export function ErrorMessage({ children, style, ...props }: TextProps<string>) {
  const theme = useTheme();

  if (!children) return null;

  return (
    <Text
      {...props}
      style={[
        {
          backgroundColor: theme.colors.errorContainer,
          borderRadius: theme.roundness,
        },
        styles.errorMessage,
        style,
      ]}
    >
      {children}
    </Text>
  );
}

export function ViewContainer({ children, style, ...props }: ViewProps) {
  const theme = useTheme();
  return (
    <View
      {...props}
      style={[{ backgroundColor: theme.colors.background }, style]}
    >
      {children}
    </View>
  );
}

export function ViewDivider() {
  return <Divider style={styles.viewDivider} />;
}

export function SignOutButton() {
  const { signOut } = useAuthenticator();
  return (
    <Button onPress={signOut} style={styles.signOutButton}>
      Sign Out
    </Button>
  );
}

export function Container({
  style,
  ...props
}: React.ComponentProps<typeof Authenticator.Container>) {
  const theme = useTheme();
  return (
    <Authenticator.Container
      {...props}
      style={[{ backgroundColor: theme.colors.background }, style]}
    />
  );
}

Override Function Calls

You can override the call to signUp, signIn, confirmSignIn, confirmSignUp, resendSignUpCode, forgotPassword and forgotPasswordSubmit functions. To override a call you must create a new services object with an async handle* function that returns an aws-amplify Auth promise.

@aws-amplify/ui-react-native v1

Use resetPassword in place of forgotPassword in version 1 @aws-amplify/ui-react-native.

The service object must then be passed into the authenticator component as a services prop. For example, let's imagine you'd like to lowercase the username and the email attributes during signUp. This would be overriden like so:

import React from 'react';
import { Button } from 'react-native';

import { Authenticator, useAuthenticator } from '@aws-amplify/ui-react-native';
import { Amplify } from 'aws-amplify';
import { SignUpInput, signUp } from 'aws-amplify/auth';
import awsconfig from './aws-exports'; Amplify.configure(awsconfig); function SignOutButton() { const { signOut } = useAuthenticator(); return <Button title="Sign Out" onPress={signOut} />; } function App() { return ( <Authenticator.Provider> <Authenticator services={{ handleSignUp: ({ username, password, options }: SignUpInput) =>
signUp({
username: username.toLowerCase(), password, options: { ...options, userAttributes: { ...options?.userAttributes, email: options?.userAttributes?.email?.toLowerCase(), }, }, }), }} > <SignOutButton /> </Authenticator> </Authenticator.Provider> ); } export default App;
Sign Up Auto Sign In

When overriding signUp in @aws-amplify/ui-react-native version 1, you must include the autoSignIn key and set enabled to true, as shown in the example below.

  import React from 'react';
  import { Button } from 'react-native';
  import { Authenticator, useAuthenticator } from '@aws-amplify/ui-react-native';
import { Amplify, Auth } from 'aws-amplify';
function SignOutButton() { const { signOut } = useAuthenticator(); return <Button title="Sign Out" onPress={signOut} />; } const services = { async handleSignUp(input) { let { username, password, attributes } = input; // custom username username = username.toLowerCase(); attributes.email = attributes.email.toLowerCase(); return Auth.signUp({ username,
password,
attributes, autoSignIn: { enabled: true, },
});
}, }; function App() { return ( <Authenticator.Provider> <Authenticator services={services} > <SignOutButton /> </Authenticator> </Authenticator.Provider> ); } export default App;

Each handle* function accepts as input the expected input of its corresponding Auth function, allowing you to override the Auth function call from within the handle* function. Here is a table of each override function name, and the values passed as input.

Auth Function CallOverride Nameinput Properties
signUphandleSignUp{username, password}
signInhandleSignIn{username, password}
confirmSignInhandleConfirmSignIn{challengeResponse}
confirmSignUphandleConfirmSignUp{username, confirmationCode}
resendSignUpCodehandleResendSignUpCode{username}
resetPasswordhandleForgotPassword{username}
confirmResetPasswordhandleForgotPasswordSubmit{username, newPassword, confirmationCode}
Function CallOverride NameformData Properties
Auth.signUphandleSignUp{username, password, attributes}
Auth.signInhandleSignIn{username, password}
Auth.confirmSignInhandleConfirmSignIn{user, code, mfaType}
Auth.confirmSignUphandleConfirmSignUp{username, code}
Auth.resendSignUpCodehandleResendSignUpCode{username}
Auth.forgotPasswordhandleForgotPassword{username}
Auth.forgotPasswordSubmithandleForgotPasswordSubmit{username, code, password}

Internationalization (I18n)

The Authenticator ships with translations for:

  • en – English (default)
  • zh – Chinese
  • nl – Dutch
  • fr – French
  • de – German
  • he – Hebrew
  • id – Indonesian
  • it – Italian
  • ja – Japanese
  • ko – Korean
  • nb - Norwegian
  • pl – Polish
  • pt – Portuguese
  • ru – Russian
  • es – Spanish
  • sv – Swedish
  • th - Thai
  • tr – Turkish
  • ua – Ukrainian

These translations can be customized using the Amplify JS' I18n module:

Note: The import path for i18n changed from aws-amplify to aws-amplify/utils in aws-amplify@6

import { I18n } from 'aws-amplify/utils';
import { translations } from '@aws-amplify/ui';
I18n.putVocabularies(translations);
I18n.setLanguage('fr');

I18n.putVocabularies({
  fr: {
    'Sign In': 'Se connecter',
    'Sign Up': "S'inscrire",
  },
  es: {
    'Sign In': 'Registrarse',
    'Sign Up': 'Regístrate',
  },
});

The list of available keys are available here.

Confirm Sign Up Page Translations

The confirm sign up page has a few specialized strings that can be translated. These include:

`Your code is on the way. To log in, enter the code we emailed to`

`Your code is on the way. To log in, enter the code we texted to`

`Your code is on the way. To log in, enter the code we sent you. It may take a minute to arrive.`

`It may take a minute to arrive.`
Translations Needed 📖

If you see any missing translations or would like to contribute a new language, we greatly appreciate contributions to translations we have here.

Labels & Text

Using the same techniques as Internationalization (I18n), you can customize the labels and text of the components:

Sign In

I18n.putVocabulariesForLanguage('en', {
  'Sign In': 'Login', // Tab header
  'Sign in': 'Log in', // Button label
  'Sign in to your account': 'Welcome Back!',
  Username: 'Enter your username', // Username label
  Password: 'Enter your password', // Password label
  'Forgot your password?': 'Reset Password',
});

Sign Up

I18n.putVocabulariesForLanguage('en', {
  'Create Account': 'Register', // Link text
  'Create a new account': 'New User', // Header text
  'Confirm Password': 'Confirm your password', // Confirm Password label
  Email: 'Enter your email',
  'Phone Number': 'Enter your phone number',
});

Forgot Password

I18n.putVocabulariesForLanguage('en', {
  'Reset your password': 'Forgot your password?',
  'Enter your username': 'Username or Email',
  'Send code': 'Reset my password',
  'Back to Sign In': 'Back to Login',
});

Setup TOTP

I18n.putVocabulariesForLanguage('en', {
  Code: '2FA Code',
  Confirm: 'Confirm 2FA',
  'Back to Sign In': 'Back to Login',
});

Form Field Customization

Extend or override the existing fields for an Authenticator subcomponent by passing an array of field option objects to the fields prop:

import React from 'react';
import { Button, StyleSheet, View } from 'react-native';

import { Authenticator, useAuthenticator } from '@aws-amplify/ui-react-native';
import { Amplify } from 'aws-amplify';

import awsconfig from './aws-exports';
Amplify.configure(awsconfig);

function SignOutButton() {
  const { signOut } = useAuthenticator();
  return <Button onPress={signOut} title="Sign Out" />;
}

function App() {
  return (
    <Authenticator.Provider>
      <Authenticator
        components={{
          SignUp: ({ fields, ...props }) => (
            <Authenticator.SignUp
              {...props}
              fields={[
                ...fields,
                {
                  name: 'preferred_username',
                  label: 'Preferred Username',
                  type: 'default',
                  placeholder: 'Enter your preferred username',
                },
              ]}
            />
          ),
        }}
      >
        <View style={style.container}>
          <SignOutButton />
        </View>
      </Authenticator>
    </Authenticator.Provider>
  );
}

const style = StyleSheet.create({
  container: { flex: 1, alignItems: 'center', justifyContent: 'center' },
});

export default App;

Label hidden

Default fields include label values. To render the fields without labels:

import React from 'react';
import { Button, StyleSheet, View } from 'react-native';

import { Authenticator, useAuthenticator } from '@aws-amplify/ui-react-native';
import { Amplify } from 'aws-amplify';

import awsconfig from './aws-exports';
Amplify.configure(awsconfig);

function SignOutButton() {
  const { signOut } = useAuthenticator();
  return <Button onPress={signOut} title="Sign Out" />;
}

function App() {
  return (
    <Authenticator.Provider>
      <Authenticator
        components={{
          SignIn: ({ fields, ...props }) => (
            <Authenticator.SignIn
              {...props}
              fields={fields.map((field) => ({ ...field, labelHidden: true }))}
            />
          ),
        }}
      >
        <View style={style.container}>
          <SignOutButton />
        </View>
      </Authenticator>
    </Authenticator.Provider>
  );
}

const style = StyleSheet.create({
  container: { flex: 1, alignItems: 'center', justifyContent: 'center' },
});

export default App;

Styling

Theme Provider

You can update the style of the Authenticator by wrapping it with a ThemeProvider. Then create a theme object, with all your font customizations.

import React from 'react';

import { Authenticator, ThemeProvider } from '@aws-amplify/ui-react-native';
import { Amplify } from 'aws-amplify';

import config from './aws-exports';

Amplify.configure(config);

function App() {
  return (
    <ThemeProvider
      theme={{
        tokens: {
          colors: {
            primary: {
              10: '{colors.pink.10}',
              20: '{colors.pink.20}',
              40: '{colors.pink.40}',
              60: '{colors.pink.60}',
              80: '{colors.pink.80}',
              90: '{colors.pink.90}',
              100: '{colors.pink.100}',
            },
          },
        },
      }}
    >
      <Authenticator.Provider>
        <Authenticator />
      </Authenticator.Provider>
    </ThemeProvider>
  );
}

export default App;

If you have TypeScript enabled, all the object keys will be present when creating the theme object. This will help speed up your development time while creating themes.

Dark mode

Amplify UI comes with a default dark mode implementation

import React from 'react';
import { useColorScheme } from 'react-native';

import {
  Authenticator,
  defaultDarkModeOverride,
  ThemeProvider,
} from '@aws-amplify/ui-react-native';
import { Amplify } from 'aws-amplify';

import config from './aws-exports';

Amplify.configure(config);

function App() {
  const colorMode = useColorScheme();
  return (
    <ThemeProvider
      colorMode={colorMode}
      theme={{
        tokens: {
          colors: {
            primary: {
              10: '{colors.pink.10}',
              20: '{colors.pink.20}',
              40: '{colors.pink.40}',
              60: '{colors.pink.60}',
              80: '{colors.pink.80}',
              90: '{colors.pink.90}',
              100: '{colors.pink.100}',
            },
          },
        },
        overrides: [defaultDarkModeOverride],
      }}
    >
      <Authenticator.Provider>
        <Authenticator />
      </Authenticator.Provider>
    </ThemeProvider>
  );
}

export default App;

Learn more about dark mode

Amplify open source software, documentation and community are supported by Amazon Web Services.

© 2025 Amazon Web Services, Inc. and its affiliates. All rights reserved. View the site terms and privacy policy.

Flutter and the related logo are trademarks of Google LLC. We are not endorsed by or affiliated with Google LLC.