Login and Forgot Password Flows
Build secure and user-friendly login and password recovery pages with proper form handling, validation, and helpful messaging.
Login and password recovery are critical touchpoints in the customer journey. Clear forms, helpful error messages, and smooth flows keep customers engaged rather than frustrated.
Login Template Structure
{# templates/customers/login.liquid #}
{% section 'customer-login' %}Login Section
{# sections/customer-login.liquid #}
<section class="customer-login section-{{ section.id }}"> <div class="container container--narrow"> <div class="customer-login__wrapper"> {# Login form #} <div class="customer-login__form-wrapper" id="login-form"> <h1 class="customer-login__heading">{{ section.settings.login_heading }}</h1>
{%- form 'customer_login', class: 'customer-login__form' -%} {%- if form.errors -%} <div class="customer-login__errors" role="alert"> <p>{{ form.errors | default_errors }}</p> </div> {%- endif -%}
<div class="customer-login__fields"> <div class="customer-login__field"> <label for="login-email">Email</label> <input type="email" id="login-email" name="customer[email]" autocomplete="email" required autofocus > </div>
<div class="customer-login__field"> <label for="login-password">Password</label> <input type="password" id="login-password" name="customer[password]" autocomplete="current-password" required > </div> </div>
<button type="submit" class="customer-login__submit button button--primary"> {{ section.settings.login_button }} </button>
<div class="customer-login__links"> <a href="#recover" class="customer-login__forgot" data-toggle-recover> Forgot your password? </a> </div> {%- endform -%}
<div class="customer-login__footer"> <p> New customer? <a href="{{ routes.account_register_url }}">Create an account</a> </p> </div> </div>
{# Password recovery form #} <div class="customer-login__recover-wrapper" id="recover-form" hidden> <h2 class="customer-login__heading">{{ section.settings.recover_heading }}</h2> <p class="customer-login__recover-text">{{ section.settings.recover_text }}</p>
{%- form 'recover_customer_password', class: 'customer-login__form' -%} {%- if form.posted_successfully? -%} <div class="customer-login__success" role="status"> <p>{{ section.settings.recover_success }}</p> </div> {%- endif -%}
<div class="customer-login__field"> <label for="recover-email">Email</label> <input type="email" id="recover-email" name="email" autocomplete="email" required > </div>
<button type="submit" class="customer-login__submit button button--primary"> {{ section.settings.recover_button }} </button>
<div class="customer-login__links"> <a href="#login" class="customer-login__back" data-toggle-login> ← Back to login </a> </div> {%- endform -%} </div> </div> </div></section>
<script> // Toggle between login and recover forms document.addEventListener('DOMContentLoaded', function() { const loginForm = document.getElementById('login-form'); const recoverForm = document.getElementById('recover-form');
// Handle toggle to recover document.querySelectorAll('[data-toggle-recover]').forEach(link => { link.addEventListener('click', function(e) { e.preventDefault(); loginForm.hidden = true; recoverForm.hidden = false; recoverForm.querySelector('input[type="email"]').focus(); window.location.hash = 'recover'; }); });
// Handle toggle to login document.querySelectorAll('[data-toggle-login]').forEach(link => { link.addEventListener('click', function(e) { e.preventDefault(); recoverForm.hidden = true; loginForm.hidden = false; loginForm.querySelector('input[type="email"]').focus(); window.location.hash = ''; }); });
// Check hash on load if (window.location.hash === '#recover') { loginForm.hidden = true; recoverForm.hidden = false; } });</script>
{% schema %}{ "name": "Customer Login", "settings": [ { "type": "header", "content": "Login Form" }, { "type": "text", "id": "login_heading", "label": "Login heading", "default": "Login" }, { "type": "text", "id": "login_button", "label": "Login button text", "default": "Sign In" }, { "type": "header", "content": "Password Recovery" }, { "type": "text", "id": "recover_heading", "label": "Recovery heading", "default": "Reset Your Password" }, { "type": "textarea", "id": "recover_text", "label": "Recovery description", "default": "We'll send you an email to reset your password." }, { "type": "text", "id": "recover_button", "label": "Recovery button text", "default": "Submit" }, { "type": "text", "id": "recover_success", "label": "Recovery success message", "default": "We've sent you an email with a link to reset your password." } ]}{% endschema %}Reset Password Template
After clicking the email link, customers land on the reset password page:
{# templates/customers/reset_password.liquid #}
{% section 'customer-reset-password' %}{# sections/customer-reset-password.liquid #}
<section class="customer-reset section-{{ section.id }}"> <div class="container container--narrow"> <h1 class="customer-reset__heading">{{ section.settings.heading }}</h1> <p class="customer-reset__text">{{ section.settings.text }}</p>
{%- form 'reset_customer_password', class: 'customer-reset__form' -%} {%- if form.errors -%} <div class="customer-reset__errors" role="alert"> {{ form.errors | default_errors }} </div> {%- endif -%}
<div class="customer-reset__fields"> <div class="customer-reset__field"> <label for="reset-password">New password</label> <input type="password" id="reset-password" name="customer[password]" autocomplete="new-password" required minlength="5" > </div>
<div class="customer-reset__field"> <label for="reset-password-confirm">Confirm new password</label> <input type="password" id="reset-password-confirm" name="customer[password_confirmation]" autocomplete="new-password" required minlength="5" > </div> </div>
<button type="submit" class="customer-reset__submit button button--primary"> {{ section.settings.button_text }} </button> {%- endform -%} </div></section>
{% schema %}{ "name": "Reset Password", "settings": [ { "type": "text", "id": "heading", "label": "Heading", "default": "Reset Your Password" }, { "type": "text", "id": "text", "label": "Description", "default": "Enter a new password for your account." }, { "type": "text", "id": "button_text", "label": "Button text", "default": "Reset Password" } ]}{% endschema %}Account Activation Template
For invite-only stores or B2B:
{# templates/customers/activate_account.liquid #}
{% section 'customer-activate' %}{# sections/customer-activate.liquid #}
<section class="customer-activate section-{{ section.id }}"> <div class="container container--narrow"> <h1 class="customer-activate__heading">{{ section.settings.heading }}</h1> <p class="customer-activate__text">{{ section.settings.text }}</p>
{%- form 'activate_customer_password', class: 'customer-activate__form' -%} {%- if form.errors -%} <div class="customer-activate__errors" role="alert"> {{ form.errors | default_errors }} </div> {%- endif -%}
<div class="customer-activate__fields"> <div class="customer-activate__field"> <label for="activate-password">Create password</label> <input type="password" id="activate-password" name="customer[password]" autocomplete="new-password" required minlength="5" > </div>
<div class="customer-activate__field"> <label for="activate-password-confirm">Confirm password</label> <input type="password" id="activate-password-confirm" name="customer[password_confirmation]" autocomplete="new-password" required minlength="5" > </div> </div>
<button type="submit" class="customer-activate__submit button button--primary"> {{ section.settings.button_text }} </button>
<p class="customer-activate__decline"> <a href="{{ routes.account_url | append: '/decline' }}"> {{ section.settings.decline_text }} </a> </p> {%- endform -%} </div></section>
{% schema %}{ "name": "Activate Account", "settings": [ { "type": "text", "id": "heading", "label": "Heading", "default": "Activate Your Account" }, { "type": "text", "id": "text", "label": "Description", "default": "Create a password to activate your account." }, { "type": "text", "id": "button_text", "label": "Button text", "default": "Activate Account" }, { "type": "text", "id": "decline_text", "label": "Decline link text", "default": "Decline invitation" } ]}{% endschema %}Login Error Handling
Common login errors and how to handle them:
{%- if form.errors -%} <div class="customer-login__errors" role="alert"> {# Shopify returns generic error for security #} <p> <strong>Login failed.</strong> Please check your email and password and try again. </p> </div>{%- endif -%}Note: Shopify intentionally returns generic errors for login to prevent email enumeration attacks.
Remember Me Functionality
Shopify handles session persistence automatically. Sessions last 2 weeks by default. You can add a visual “Remember me” checkbox for user confidence:
<div class="customer-login__remember"> <label class="customer-login__checkbox-label"> <input type="checkbox" checked disabled> <span>Remember me</span> </label> <span class="customer-login__remember-info"> Stay logged in for 2 weeks </span></div>Login Styles
.customer-login { padding: var(--spacing-2xl) 0; min-height: 60vh; display: flex; align-items: center;}
.customer-login__wrapper { width: 100%; max-width: 400px; margin: 0 auto;}
.customer-login__heading { text-align: center; font-size: clamp(1.75rem, 4vw, 2.25rem); margin-bottom: var(--spacing-lg);}
.customer-login__fields { display: flex; flex-direction: column; gap: var(--spacing-md);}
.customer-login__field label { display: block; font-size: 0.875rem; font-weight: 500; margin-bottom: var(--spacing-xs);}
.customer-login__field input { width: 100%; padding: var(--spacing-sm) var(--spacing-md); border: 1px solid var(--color-border); border-radius: var(--border-radius); font-size: 1rem;}
.customer-login__field input:focus { outline: none; border-color: var(--color-primary); box-shadow: 0 0 0 3px rgba(var(--color-primary-rgb), 0.1);}
.customer-login__submit { width: 100%; margin-top: var(--spacing-lg);}
.customer-login__errors { background: #fef2f2; border: 1px solid #fecaca; color: #b91c1c; padding: var(--spacing-md); border-radius: var(--border-radius); margin-bottom: var(--spacing-lg);}
.customer-login__success { background: #d1fae5; border: 1px solid #6ee7b7; color: #065f46; padding: var(--spacing-md); border-radius: var(--border-radius); margin-bottom: var(--spacing-lg);}
.customer-login__links { text-align: center; margin-top: var(--spacing-md);}
.customer-login__forgot { font-size: 0.875rem; color: var(--color-text-light);}
.customer-login__footer { text-align: center; margin-top: var(--spacing-xl); padding-top: var(--spacing-xl); border-top: 1px solid var(--color-border);}
.customer-login__recover-text { text-align: center; color: var(--color-text-light); margin-bottom: var(--spacing-lg);}
.customer-login__back { display: inline-block; margin-top: var(--spacing-md); font-size: 0.875rem;}Security Best Practices
- Use HTTPS: Shopify handles this automatically
- Autocomplete attributes: Help password managers
- Generic error messages: Don’t reveal if email exists
- Rate limiting: Shopify handles this server-side
- Password requirements: Minimum 5 characters (Shopify enforced)
Redirect After Login
Control where customers go after logging in:
{# In login form, add return_to parameter #}{%- form 'customer_login' -%} <input type="hidden" name="return_to" value="{{ request.path }}"> {# form fields #}{%- endform -%}Or use URL parameter:
/account/login?return_to=/collections/exclusivePractice Exercise
Build complete login and recovery flows:
- Login form with email and password
- Toggle to password recovery form
- Reset password template
- Success and error states
Test by:
- Logging in with valid credentials
- Testing with wrong password
- Completing password recovery flow
- Checking redirect behavior
Key Takeaways
customer_loginform for authenticationrecover_customer_passwordfor email recoveryreset_customer_passwordfor new password- Toggle UI between login and recover
- Generic errors for security
return_tofor redirect control- Activation for invite-only accounts
What’s Next?
The next lesson covers the Account Overview page with orders and addresses.
Finished this lesson?
Mark it complete to track your progress.
Discussion
Loading comments...