XXX for Craft CMS websites XXXX



General TODO:
Replace links to Craft CMS 3 with links to Craft CMS 4? Maybe state which version this is for?

Just reading this text without visual presentation is a bit hard to follow for me (Frank). It almost sounds like a transcript of a video, like half of the presentation layer is missing. Maybe by adding a lot of images for context, it's better? Some video at the beginning of what this looks like in the browser?!

It's also very detailed and crazy loooooooooooooooong. ChatGPT: Write a long article about Craft CMS custom login forms.

Please replace about 10 occurrences of "It is worth noting" with something else :)

TODO, headline, What's this about?

TODO: Write a proper introduction what to expect and why this article exists (not enough docs at Craft CMS), deep dive, etc. Extend on the sentences below.

In this article, we will present user management templates for Craft CMS. These will include:

  • Login form. Ajax-based, displayed in a modal or on a separate page.
  • Registration and profile edit forms, automatically outputting any additional fields assigned to the user field layout.
  • Password reset and set new password forms.
  • Address form and list of addresses. Address form is also automatically outputting additional fields assigned to the address field layout.
  • User widget showing currently signed-in user and links to user management pages.

There is a [GitHub repository](TODO: LINK!) accompanying this article, where the whole Twig code lives. You can clone it and see how these templates work in practice or copy them into your Craft CMS project.

Base layout

TODO: Insert image here!

Each page is represented by a Twig file in the main templates directory. Thanks to that, they can be accessed under the URL same as their filename (except address pages which use custom routes). All of them extend the _base.twig file which is just a simple layout containing things like <head> and <body> tags - this file exists just to better showcase user management pages. All templates use bulma CSS framework classes - the Bulma CDN file is linked from the _base.twig file. These classes can however be easily switched to something else thanks to modular structure of templates.

The _base.twig file includes the user widget file (_user/partials/user-widget), which shows the currently signed-in user and links to the user management pages. Craft CMS injects the currentUser variable into the template which contains the currently logged-in user object or if nobody is logged in, it is set to null. Using this variable we choose which user management links to show - login and registration or profile, addresses and logout. Note that logout is not actually a page but a route built-in into Craft CMS which can be changed using the logoutPath general config variable.

Below the user widget, there is a flash message component _user/partials/flash-messages. Flash messages are displayed only once and are used to show messages related to forms - errors or success notifications. Within that component we adjust the types of messages set by Craft CMS to CSS classes available in Bulma.

Finally, we display a header which uses the pageTitle variable defined in the specific pages and the content block where specific pages inject their content.

Now, let's go through every template - starting with the login form.

Login form

The login form page is available under the login address and lives in the login.twig template file. If you want to change this file name to something else, remember to also change loginPath in the general config. By default, Craft CMS redirects users to login URL when they need to sign in.

The login.twig file contains requireGuest Twig tag. If the user is already logged in, there is no need to see the login form, so the system will redirect the user either to path defined in postLoginRedirect general config setting (by default it is home page) or to login restricted page user tried to access.

Now let's go through the _user/login-form file - it contains the actual login form. First, we check if autofocus is enabled by the variable passed into the template (if no variable is set, it defaults to true). Why? If we visit the login page, the login form itself will be present in two instances - one that we immediately see and also one hidden, living in a modal. If we always allowed autofocus, the modal login form could grab the focus instead of the regular login form.

Next, we define the HTML attributes of each part of the login form as Twig objects which will be later used to generate inputs using the (tag)[] Twig function. These objects are located at the beginning of the template file for a sake of convenience - so they can be easily modified.

The loginName variable is set value from POST request, so when we submit a login form with the wrong password or username, the previously entered value would be still present in input after form refresh. If we display form without submitting it before, loginName is set to - username of previously logged-in user stored in the server session, which will just be null if there is no such value stored.

If we look at inputUsernameAttributes object, we might notice that the inputs autofocus property depends on previously defined enableAutofocus but also on loginName - if there is login name already present in the username input, we don't need to focus here and can focus in password input instead. You might also notice that we also set data-enable-autofocus attribute - this will be used for autofocus functionality in the login modal.

It is also worth noting that the settings objects use static translation with the app param. This means that Craft CMS built-in static translations are used and our forms will be automatically translated if Craft provides a translation for a specific language.

After defining form contents attributes, we use the Twig embed tag to extend the base form template - _user/base-form.twig. This form contains a <form> with default attributes (which we can add to or overwrite by passing the formAttributes variable into this template) and CSRF protection functionality. There is also a commented out _user/partials/model-errors component included - it displays all potential errors on top of the form, but by default it is not needed because inputs are set to display any possible errors next to a specific input.

Now let's look at the formContent block where we place form contents. First we need to take care of some technical fields which are not visible but make sure that the login form works correctly. actionInput will direct form request to the proper login controller.

TODO: Review my corrections, I am not sure if correct. I don't get the following sentences fully. Maybe make them shorter?

redirectInput is set to the value, which will redirect the user after a successful login to the login restricted page he wanted to visit (or to address set in the postLoginRedirect general config setting if we just visited login page directly).

Then, we output the form fields using Twig macros. Thanks to using macros, all our forms are uniform and can be easily modified globally. Let's take a look at _user/partials/form-macros where macros are defined, starting with the formFieldInput which generates text inputs.

First, we define the default input class (input) and if the errors property was passed with settings, we also add a class which will mark the input as invalid (is-danger by default). Note that these classes will be overwritten if we use class property in the object passed to the macro.

Then, we output the label, based by the label property in the macro params. The label has its own macro where the default CSS class is set (label). Note that the for attribute of the label is automatically set to the id attribute of the input.

After that, we set some ARIA attributes on the input, related to possible errors - aria-invalid and aria-errormessage which is set to the ID of element containing the error message.

Finally we output the input using the tag() function. Both, label and input HTML code, is set to the html variable which is then passed to the formField macro which contains the field wrapper common for all form widgets (it is used by other macros such as formFieldCheckbox, formFieldSelect or formFieldButton). In this macro we also finally output any potential error messages. Note that we did not actually defined any errors in login form. Any possible errors are related to models like user or address and login form are not populated by any model.

TODO: Clarify the following sentence, don't get it.

Only potential error message we will see is one displayed in flash messages.

Ajax Login

Our login form is now fully functional - but we can still make it better by adding AJAX functionality. We will use the progressive enhancement strategy - our form will work perfectly fine if Javascript is disabled and AJAX is not available. Thanks to using AJAX, our login form will not needlessly reload the whole page in case of an unsuccessful login and just display an error message instead. This is a better user experience.

The AJAX functionality code is placed within the js Twig tag, wrapped with a self-executing function to prevent any variable conflicts. Let's walk through this code. First, we define messages which will be shown to the user. Unfortunately, such messages are not present in built-in Craft static message translations, so we use just regular static translations.

To grab the login form DOM elements, we use the data-login-form attribute. This is the same attribute we earlier set in the Twig variable formAttributes and passed to the _user/base-form. Then we loop through the DOM element collection, to make sure AJAX functionality is applied to both the regular login form and the login form present in the modal. Within the forEach loop we attach to the submit event and use e.preventDefault() to prevent the regular submission of the login form. Then we serialize form values using to FormData object and create an AJAX request. Before the request starts, we add a loading class to the submit button (class itself is taken from data-login-form-button-loading-class of the button) and a disabled attribute to the fieldset element - this will make the form disabled until the requests completes, to avoid sending multiple requests at once.

When the request is complete, we remove the loading class from the button and the disabled attribute from the fieldset, parse the server response from JSON string, and act depending on response code:

  • response code 200, login successful - the success message is put into the submit button. We either redirect the user to URL returned by server or just refresh the page if no redirect address was provided.
  • response code 400, login unsuccessful - we display an error message returned by server using the regular alert() function.
  • response code 500, server error - we display a generic error message using the regular alert() function.
  • response code 0, connection error - we display a network error message using the regular alert() function.

Login modal

The login form can be also displayed in a modal, using the Modal component plugin - make sure you have it installed if you want to use it that way. If we combine that with AJAX functionality, we get a very responsive and dynamic login form.

The login modal is placed in _user/partials/user-widget file so it is available on every page. The modal itself works by providing a modal template which can have a modal content passed to it using the embed Twig tag - we just pass the login form there, with the enableAutofocus variable set to false. We also namespace the id and for attributes (using apply Twig function to apply namespace filter) of elements inside to prevent conflicts and focusing on wrong input when someone clicks label.

The modal is opened by clicking on the regular login link in user widget which has data-a11y-dialog-show attribute set to login-modal - the same as modalId variable passed to modal component. To make sure that the link does not redirect us to the login page just after opening modal, we attach a click event to it and run preventDefault() function. Keep in mind however that we can still open the login page in a new tab using a middle mouse button.

We also make sure that the autofocus functionality works after opening the modal - we use modal component plugin event show for that. After opening the modal we just focus on input that has data-enable-autofocus attribute present.

Reset password form

This form allows users to send a password reset email. The password reset page lives in the reset-password.twig file and the password reset form is in _user/reset-password-form.twig. It is a pretty simple form, with just one input, and just like the login form it extends the _user/base-form.twig base form and uses macros from the _user/partials/form-macros.twig file.

It is worth noting that you can set the general config variable setPasswordRequestPath to reset-password so that Craft will redirect .well-known/change-password requests to this URI.

Set password form

This form allows users to set new password after clicking the password reset link delivered by email. It is worth noting that Craft will provide its own form for that if we don't define our own. The set password page lives in the setpassword.twig file and the form is in _user/set-password-form.twig.

It's worth noting that the page URL is set in general config under setPasswordPath to the default setpassword value - if you want your set password page live under a different URL, change this setting to something else.

The form itself is pretty simple, however it is worth noting that it has two additional hidden inputs - code and id, which are filled by Twig variables injected automatically into a template based on URL parameters. Note that if you reset password of admin user, the frontend form will NOT be used - default control panel based form will be used instead.

Registration form

The registration page lives in the register.twig file. Here we place requireGuest Twig tag to make sure someone already logged-in gets redirected when trying to visit the registration page. We also include a registration form - _user/register-form.twig.

In the registration form file we define the formUser object containing the user object. When we submit the form and it fails due to some errors, Craft CMS injects the user variable with our previously entered values into the template so that the form can be re-populated. If we visit the form for the first time and the user variable is missing, we need to create empty an User object instead using the create function.

The form file is very simple - most of its functionality is actually placed outside, in the _user/partials/user-fields.twig file which is included inside the form. That's because most of the registration form contents are shared with the profile form. Inside the user fields file, first we define the fields attributes and then output the fields using macros. Note that we show the username field only if useEmailAsUsername is set to false - if we use email as username, there is no need for a separate username field. We also show the "new password" field used to change the user password only if user object has an id property - which means that we edit the existing user object and are on the profile page instead of the registration page.

The most interesting part however is outputting the custom fields assigned to the user field layout. We grab them using the formUser.getFieldLayout().customFields property and loop through them and output of each using the _user/partials/custom-fields template. Inside that file, depending on the field class, we output its widget using a specific macro. As for now, text, lightswitch and dropdown fields are supported - the rest of the fields are ignored.

Profile form

The profile form works very similarly to the registration form. The profile page lives in profile.twig. In here, we use the requireLogin Twig tag to make sure that the user is logged in before viewing the profile page. We also include the profile form - _user/profile-form.twig. This time formUser is set to either the user variable or to the currentUser which is injected into the template by Craft CMS and contains the currently logged-in user. Inside the form we also place a hidden input with name userId and the value is set to - so that Craft knows which user to edit. Besides that thanks to the shared _user/partials/user-fields.twig template, the profile form works pretty much the same as the registration form - it just displays an additional "new password" field so users can change their passwords.


Addresses are element types that were introduced in Craft CMS 4. They are assigned to users and can have their own field layout composed of native fields (pre-existing, specific only to address, like for example country or postal code) as well as regular Craft fields. To manage them on the frontend, we need to have three pages - addresses list, new address page, and edit address page.

Users can have multiple addresses assigned - which means that user needs to be able to edit multiple addresses. Since Craft does not provide URLs for addresses, we need to set up our own route which will include address ID. Using this ID, our template code will perform element query and grab specific address to populate address form.

Three routes for addresses pages are defined in the config/routes.php file. Note how template files which routes direct to have underscores at the start of filenames - this is to avoid using automatic template file routing.

Address list

The address list page lives in _address-list.twig and is available under the addresses URI. The page contents code is located in the _users/address-list-content.twig file. Here we grab all addresses belonging to the currently logged-in user using currentUser.getAddresses() and loop through them. With each loop we display the address title, edit link and deletion form. The deletion form itself directs to users/delete-address controller action.

On the bottom of the page we also display a link to new address page.

New address page

The new address page lives in the _address-new.twig template, while new address form is located in _user/address-new-form.twig. Just like with profile and registration pages, new address and edit address pages use common template, _user/partials/address-fields.twig. Note that address page defines address variable - set either to address variable injected into template when we submit form which contains incorrect values or to empty address object.

Within the address fields template, we loop through address field layout elements. These consist of both regular craft field which are just outputted using _user/partials/custom-fields.twig template (which is also used by register and profile forms) or in the case of native fields - we manually output inputs. Note that single native field is sometimes represented by multiple inputs - for example, Latitude/Longitude field is represented by two separate inputs in the form - one for latitude and one for longitude.

Edit address page

Edit address page is located in _address-single.twig template. In this file we also perform element query, using addressId variable injected into template by the route. If no address with specific ID is found, we throw 404 error. Using queried address object, we also define pageTitle containing address title.

The edit address form lives in the _user/address-edit-form.twig file and it works pretty much like new address form.


Share & discuss this: