Amplify UI

Customization

Override and customize your Authenticator.

Headers & Footers

All of the Authenticator views have a headerContent and footerContent argument in their initializers, which you can use to customize the UX.

Authenticator

Adding a header and/or footer to the Authenticator component will display it in all of its views. By default, no header nor footer is provided by this component.

This example will result in the provided header (a logo) and footer (the copyright disclaimer) to be displayed in all views:

Authenticator(
    headerContent: {
        Image("amplify_logo")
    },
    footerContent: { 
        Text("© All Rights Reserved")
    }
) { _ in
    Text("You are logged in!")
}
Note

The content that the Authenticator displays once the user has signed in, i.e. the Text("You are logged in!") in the previous example, is not consider an Authenticator view and as such no header nor footer will be displayed for it.

Content Views

You can also provide a header and/or footer individually to each view that represents a step (e.g. SignInView, SignUpView, etc).

These views have the following default values:

  • Header: A view that displays the step title, e.g. "Sign In"
  • Footer: Any link buttons displayed by default (e.g. the "Forgot Password?" button in SignInView), otherwise empty

This example sets a custom header and footer in the SignInView, which will only be displayed on that step:

Authenticator(
    signInContent: { state in
        SignInView(
            state: state,
            headerContent: {
                VStack {
                    SignInHeader() // Re-use the default header
                    Divider() // Add a Divider
                }
            },
            footerContent: {
                VStack {
                    Divider() // Add a Divider
                    SignInFooter() // Re-use the default footer
                }
            }
        )
    }
) { _ in
    Text("You are logged in!")
}

Internationalization (I18n)

The Authenticator ships with translations for English. These values can be customized simply by defining your own Localizable.strings file in your app and overriding the desired keys.

For example, you can override the Sign In title and buttons texts:

"authenticator.signIn.title" = "Login";
"authenticator.signIn.button.forgotPassword" = "Reset password";
"authenticator.signIn.button.signIn" = "Submit";
"authenticator.signIn.button.createAccount" = "New user?";

Labels & Text

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

"authenticator.field.username.label" = "Enter your username";
"authenticator.field.username.placeholder" = "Your username is case-sensitive";
"authenticator.field.password.label" = "Enter your password";
"authenticator.field.password.placeholder" = "Your password is case-sensitive";

Sign Up Field Order

The Authenticator respects the order of the Sign Up fields that are provided in the signUpFields(_:) call.

If the Authenticator automatically adds a required missing attribute (e.g. the verification mechanism), it will appear at the end.

Field Customization

When providing Sign Up Fields, you can use the default ones or define your own:

Authenticator { _ in
    Text("You are logged in!")
}.signUpFields([
    .username() // Default field for username
    .password(isRequred: false) // Makes password optional
    .text( // A customized text field for the Website attribute
        key = .website, // A AuthUserAttributeKey
        label = "Website", // Custom label
        placeholder = "Enter your website" // Custom placeholder
        isRequired: true, // Makes field mandatory
        validator: { content in // Custom validator
            guard content.contains("example.com") else {
                return "Your website must have a domain of example.com"
            }
            return nil
        }
    ),
    .custom( // A fully custom field for a Custom attribute
        attributeType: .custom(attributeKey: .custom("TOS")),
        validator: { content in
            guard let selected = Bool(content), selected else {
                return "You must agree to the Terms of Service"
            }
            return nil
        }
    ) { binding in
        Toggle(isOn: binding.asBool()) {
            Text("I agree to the terms of service")
        }
    }
])

// ...
// Custom extension to convert a Binding<String> into a Binding<Bool>
extension Binding where Value == String {
    func asBool() -> Binding<Bool> {
        return .init(
            get: {
                return Bool(wrappedValue) ?? false
            },
            set: { value in
                wrappedValue = String(value)
            }
        )
    }
}

Theming

By default, the Authenticator style matches those of the Amplify UI's default theme. You can customize this look and feel by using the authenticatorTheme(_:) view modifider and providing a customized AuthenticatorTheme:

Authenticator { _ in
    // ...
}.authenticatorTheme(myCustomTheme)

AuthenticatorTheme is a class that allows you to set the style for various elements, which will be applied throughout the Authenticator views.

You can customize:

  • Spacing (horizontal and vertical): For Authenticator views, buttons, alerts and fields.
  • Colors: For backgrounds, borders and foreground elements.
  • Fonts: For all views

The following example changes the colors and fonts used in the Authenticator:

struct MyView: View {
    private let theme = AuthenticatorTheme()

    init() {
        // Colors
        theme.colors.background.interactive = .pink
        theme.colors.foreground.interactive = .red
        theme.colors.border.interactive = .pink
        
        // Fonts
        theme.fonts.title = .custom("Impact", size: 40)

        // Authenticator
        theme.components.authenticator.spacing.vertical = 20
        theme.components.authenticator.cornerRadius = 25
        theme.components.authenticator.backgroundColor = Color(uiColor: .systemGray6)
        theme.components.authenticator.padding = .init(
            top: 20,
            bottom: 20,
            trailing: 50,
            leading: 50
        )
        
        // Buttons
        theme.components.button.primary.cornerRadius = 100
        theme.components.button.primary.padding = 20
        theme.components.button.link.font = .custom("Noteworthy-Bold", size: 15)
                
        // Fields
        theme.components.field.spacing.vertical = 20
        theme.components.field.backgroundColor = .init(uiColor: .init(dynamicProvider: {
            return $0.userInterfaceStyle == .dark ? .black : .white
        })) // A dynamic color according to the color mode
        
        // Alerts
        theme.components.alert.cornerRadius = 30
        theme.components.alert.padding = 50
    }

    var body: some View {
        Authenticator { _ in
            // ...
        }
        .authenticatorTheme(theme)
    }
}

Full UI Customization

Customize the UI for the Authenticator flows

In addition to customizing fields and theming, you can also provide a custom UI for one or more of the Authenticator steps.

For simple use cases (such as adding additional information), you can use a combination of default views from the Authenticator library and views that you define yourself:

Authenticator(
    signInContent: { state in
        HStack {
            Image("custom_image")
            Divider()
            SignInView(state: state) // Re-use the default SignInView
        }
    }
) { _ in
    // ...
}

For more complex cases, you can completely replace the UI for any particular step.

Every view has an associated *State class that they observe, and that can be used to implement a custom UI. These states have @Published properties for their fields, which you can bind to your custom view.

This example shows how to fully replace the content displayed for the Sign In step, but a similar approach can be used for most steps:

Authenticator(
    signInContent: { state in
        CustomSignInView(state: state)
    }
) { _ in
    // ...
}

/// Custom Sign In view
struct CustomSignInView: View {
    @ObservedObject var state: SignInState

    var body: some View {
        VStack {
            TextField("Username", text: $state.username)
            SecureField("Password", text: $state.password)
            Divider()

            Button("Sign in") {
                Task {
                    try? await state.signIn()
                }
            }

            if state.isBusy {
                ProgressView()
            }
        }
    }
}

Customize the UI for the Sign Up flow

Customizing the Sign Up step is slightly different, as SignUpState does not individually publish each of its fields, but rather has one fields: [SignUpState.Field] property.

A SignUpState.Field is itself an @ObservableObject that has the SignUpField that it represents, and the @Published property (called value) that you need to bind to your view.

The following example shows how to fully replace the content displayed for the Sign Up step by iterating through the array of fields:

Authenticator(
    signUpContent: { state in
        CustomSignUpView(state: state)
    }
) { _ in
    // ...
}

/// Custom Sign Up view
struct CustomSignUpView: View {
    @ObservedObject var state: SignUpState

    var body: some View {
        VStack {
            ForEach(state.fields, id: \.self) { field in
                createCustomField(for: field)
            }

            Button("Sign Up") {
                Task {
                    try? await state.signUp()
                }
            }
        }
    }

    /// Creates a view for a SignUpField according to its SignUpAttribute
    @ViewBuilder
    private func createCustomField(for signUpField: SignUpState.Field) -> some View {
        @ObservedObject var observedField = signUpField
        switch signUpField.field.attributeType {
        case .username:
            TextField("Username", text: $observedField.value)
        case .password:
            SecureField("Password", text: $observedField.value)
        case .passwordConfirmation:
            SecureField("Confirm password", text: $observedField.value)
        case .phoneNumber:
            TextField("Phone Number", text: $observedField.value)
        case .email:
            TextField("Email", text: $observedField.value)
        /// ... An so on for all cases of SignUpAttribute
        }
    }
}

TOTP Setup

You can also customize the TOTP setup experience. We make available arguments in the ContinueSignInWithTOTPSetupView i.e. qrCodeContent and copyKeyContent that can help you provide custom content for the TOTP Setup Experience. In the example below, examine how you could customize the setup screen.

Authenticator(
    continueSignInWithTOTPSetupContent: { state in
        ContinueSignInWithTOTPSetupView(
            state: state,

            // Example of how a customer can pass a custom QR code
            qrCodeContent: { state in
                // Your custom QR Code implementation goes here

            },
            copyKeyContent: { state in
                // Your custom implementation goes here
            }
        )
    }
) { _ in
    // ...
}

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

© 2024 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.