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 Settings → Users → Settings, 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.
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 Utilities → System Messages → When 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 hiddeninput
element that ensures a form is posted to the right action.csrfInput()
— Outputs aninput
element that ensures a valid CSRF token is sent with the request.input()
andhiddenInput()
— Generic helpers for renderinginput
elements.redirectInput()
— Outputs a hiddeninput
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 yourloginPath
.- 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.