Front-End User Accounts

If your site needs front-end user accounts, follow these steps to get everything up and running.

Step 1: Upgrade to Craft Pro #

Support for multiple user accounts (in the control panel and front-end) is a feature of Craft Pro. If you are currently using Craft Solo, upgrade now.

Step 2: Check your User Settings #

Go to SettingsUsersSettings, and tick the Allow public registration checkbox to start accepting registration form submissions from your site’s front-end.

Without this setting enabled, Craft only allows existing control panel users with the “Register users” permission to create new accounts.

Public registration setting

While you’re there, make sure the other user settings look good.

  • We strongly recommend you keep the Verify email addresses checkbox ticked, to ensure that all of your users actually have access to the email addresses they’ve set on their accounts. (This applies to new registrations and changes to existing accounts’ email addresses.)
  • If you want your custom fields to be fully validated, be sure to tick Validate custom fields on public registration.
  • If you want a manual account approval flow, tick the Deactivate users by default checkbox. (In Craft 3, this was Suspend users by default.)
  • It’s a good idea to select a default user group (one with no permissions associated with it) so you can keep track of the publicly-registered users.

If you want your front-end users to have user photos (avatars), make sure the volume selected in the User Photo Location setting is configured with its Assets in this volume have public URLs setting enabled.

Step 3: Create the Account Management Forms #

Front-end user accounts should not typically have access to the Craft control panel, so you will need to create your own custom account management forms.

These are traditionally implemented as Twig templates, but everything we’ll cover here can also be accomplished via Ajax, if you have a decoupled front end.

Throughout the examples below, we’ll be using some special Twig functions provided by Craft to make the markup a little easier to read. They’re summarized in the Helpers section at the end of this article!

All custom fields can be updated by users; omitting a custom field from your form doesn’t protect it from being modified.

Login Form #

Create a login.twig template with a form that posts to the users/login controller action:

<form method="post" accept-charset="UTF-8">
  {{ csrfInput() }}
  {{ actionInput('users/login') }}

  <label for="loginName">Username or email</label>
  {{ input('text', 'loginName', craft.app.user.rememberedUsername, {
    id: 'loginName',
    autocomplete: 'username'
  }) }}

  <label for="password">Password</label>
  {{ input('password', 'password', null, {
    id: 'password',
  }) }}

  <label>
    {{ input('checkbox', 'rememberMe', '1') }}
    Remember me
  </label>

  <button>Login</button>

  {% if errorMessage is defined %}
    <p>{{ errorMessage }}</p>
  {% endif %}
</form>

<p><a href="{{ url('reset-password') }}">Forgot your password?</a></p>

If you’d prefer to save this template somewhere else, update your loginPath config setting with the correct path. The setting controls both the public URL path and the location of the template to be rendered. The global loginUrl variable will be set accordingly.

 

You can customize where users should get redirected to after login via the postLoginRedirect config setting.

Registration Form #

Create a register.twig template with a form that posts to the users/save-user controller action:

{% macro errorList(errors) %}
  {% if errors %}
    {{ ul(errors, {class: 'errors'}) }}
  {% endif %}
{% endmacro %}

{# `user` is defined if the form returns validation errors. #}
{% set user = user ?? null %}

<form method="post" accept-charset="UTF-8">
  {{ csrfInput() }}
  {{ actionInput('users/save-user') }}

  <label for="username">Username</label>
  {{ input('text', 'username', user.username ?? null, {
    id: 'username',
    autocomplete: 'username',
  }) }}
  {{ user ? _self.errorList(user.getErrors('username')) }}

  {# In Craft 4, `firstName` and `lastName` were combined into a single property: #}
  <label for="full-name">Full Name</label>
  {{ input('text', 'fullName', user.fullName ?? null, {
    id: 'full-name',
    autocomplete: 'name',
  }) }}
  {{ user ? _self.errorList(user.getErrors('fullName')) }}

  <label for="email">Email</label>
  {{ input('email', 'email', user.email ?? null, {
    id: 'email',
    autocomplete: 'email',
  }) }}
  {{ user ? _self.errorList(user.getErrors('email')) }}

  <label for="password">Password</label>
  {{ input('password', 'password', null, {
    id: 'password',
  }) }}
  {{ user ? _self.errorList(user.getErrors('password')) }}

  <button>Register</button>
</form>

You can skip the username field if you’ve enabled the useEmailAsUsername setting.

User Profile Form #

Create a profile.twig template with a form that also posts to the users/save-user controller action. This is similar to the registration form, except that it requires a logged-in user and includes their ID in the post data.

{# Require that a user is logged in to view this form. #}
{% requireLogin %}

{% macro errorList(errors) %}
  {% if errors %}
    {{ ul(errors, {class: 'errors'}) }}
  {% endif %}
{% endmacro %}

{# If there were any validation errors, a `user` variable will be passed to the
   template, which contains the posted values and validation errors. If that’s not
   set, we’ll default to the current user. #}
{% set user = user ?? currentUser %}

{% if user.hasErrors() %}
  <p>Unable to save your profile.</p>
{% endif %}

<form method="post" accept-charset="UTF-8" enctype="multipart/form-data">
  {{ csrfInput() }}
  {{ actionInput('users/save-user') }}
  {{ hiddenInput('userId', user.id) }}
  {{ redirectInput('users/{username}') }}

  {# In Craft 4, `firstName` and `lastName` were combined into a single property: #}
  <label for="full-name">Full Name</label>
  {{ input('text', 'fullName', user.fullName, {
    id: 'full-name',
    class: user.hasErrors('fullName') ? 'error',
    autocomplete: 'name',
  }) }}
  {{ _self.errorList(user.getErrors('fullName')) }}

  {% if user.photo %}
    <label>Photo</label>
    {{ user.photo.getImg({width: 150, height: 150})|attr({
      id: 'user-photo',
      alt: user.friendlyName,
    }) }}

    <label for="delete-photo">
      {{ input('checkbox', 'deletePhoto', '1', {
        id: 'delete-photo',
      }) }}
      Delete photo
    </label>
  {% endif %}

  <label for="photo">Upload a new photo</label>
  {{ input('file', 'photo', null, {
    id: 'photo',
    accept: 'image/png,image/jpeg',
  }) }}

  {% if not craft.app.config.general.useEmailAsUsername %}
    <label for="username">Username</label>
    {{ input('text', 'username', user.username, {
      id: 'username',
      class: user.hasErrors('username') ? 'error',
      autocomplete: 'username',
    }) }}
    {{ _self.errorList(user.getErrors('username')) }}
  {% endif %}

  <label for="email">Email</label>
  {{ input('text', 'email', user.unverifiedEmail ?? user.email, {
    id: 'email',
    class: user.hasErrors('email') ? 'error',
    autocomplete: 'email',
  }) }}
  {{ _self.errorList(user.getErrors('username')) }}

  {% if craft.app.projectConfig.get('users.requireEmailVerification') %}
    <p>New email addresses need to be verified.</p>
  {% endif %}

  <label for="new-password">New Password</label>
  {{ input('password', 'newPassword', null, {
    id: 'new-password',
    class: user.hasErrors('newPassword') ? 'error',
    autocomplete: 'new-password',
  }) }}
  {{ _self.errorList(user.getErrors('newPassword')) }}

  <label for="current-password">Current Password</label>
  {{ input('password', 'password', null, {
    id: 'current-password',
    class: user.hasErrors('currentPassword') ? 'error',
    autocomplete: 'current-password',
  }) }}
  {{ _self.errorList(user.getErrors('currentPassword')) }}

  {# Custom “Bio” field #}
  <label for="bio">Bio</label>
  {{ tag('textarea', {
    text: user.bio,
    id: 'bio',
    name: 'fields[bio]',
    class: user.hasErrors('bio') ? 'error',
  }) }}
  {{ _self.errorList(user.getErrors('bio')) }}

  <button>Save Profile</button>
</form>

You can change the name of the variable that the user is be returned to the template as if it contains validation errors, by including a userVariable input in your form.

{{ hiddenInput('userVariable', 'badUser'|hash) }}

Reset Password Forms #

Create a reset-password.twig template with a form that posts to the users/send-password-reset-email controller action:

<form method="post" accept-charset="UTF-8">
  {{ csrfInput() }}
  {{ actionInput('users/send-password-reset-email') }}

  <label for="loginName">Username or email</label>
  {{ input('text', 'loginName', loginName ?? craft.app.user.rememberedUsername, {
    id: 'loginName',
    autocomplete: 'username',
  }) }}

  {% if errors is defined %}
    {{ ul(errors, {class: 'errors'}) }}
  {% endif %}

  <button>Submit</button>
</form>

You can make this form discoverable by clients that support .well-known/change-password URLs by setting the setPasswordRequestPath config setting to 'reset-password' (or whatever path you actually end up saving this template at).

When this form is submitted with a valid username or email address, Craft will send an email to the user with a link they can click to choose a new password.

You can customize the password-reset email subject and body from UtilitiesSystem MessagesWhen someone forgets their password.

Set Password Form #

Craft provides a default template for entering a new password, but you can provide a custom one by creating a setpassword.twig template with a form that posts to the users/set-password form:

<form method="post" accept-charset="UTF-8">
  {{ csrfInput() }}
  {{ actionInput('users/set-password') }}

  {# These two values are included in a password reset URL,
     and available in the template, automatically. They must
     be passed back when setting a new password to validate
     the request: #}
  {{ hiddenInput('code', code) }}
  {{ hiddenInput('id', id) }}

  <label for="new-password">New Password</label>
  {{ input('password', 'newPassword', null, {
    id: 'new-password',
  }) }}

  {% if errors is defined %}
    {{ ul(errors, {class: 'errors'}) }}
  {% endif %}

  <button>Submit</button>
</form>

If you’d prefer to save this template somewhere else, be sure to update your setPasswordPath config setting with the correct path.

Helpers #

Here’s a quick recap of the helpers and settings we used in these forms (as well as some other related ones):

  • actionInput() — Outputs a hidden input element that ensures a form is posted to the right action.
  • csrfInput() — Outputs an input element that ensures a valid CSRF token is sent with the request.
  • input() and hiddenInput() — Generic helpers for rendering input elements.
  • redirectInput() — Outputs a hidden input element with a securely hashed URL, which the user is sent to upon a successful submission.
  • {% requireLogin %} — Twig tag to ensure a template is only rendered for logged-in users. Craft will set a return URL and redirect guests to your loginPath.
  • Session Settings — A number of options for configuring the behavior of sessions and the URLs related to logging in and out.
  • Controller Actions — More information on working with forms and actions, including support for Ajax.

Applies to Craft CMS 4 and Craft CMS 3.