Customer Account Pages Beginner 10 min read

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

  1. Use HTTPS: Shopify handles this automatically
  2. Autocomplete attributes: Help password managers
  3. Generic error messages: Don’t reveal if email exists
  4. Rate limiting: Shopify handles this server-side
  5. 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/exclusive

Practice Exercise

Build complete login and recovery flows:

  1. Login form with email and password
  2. Toggle to password recovery form
  3. Reset password template
  4. Success and error states

Test by:

  • Logging in with valid credentials
  • Testing with wrong password
  • Completing password recovery flow
  • Checking redirect behavior

Key Takeaways

  1. customer_login form for authentication
  2. recover_customer_password for email recovery
  3. reset_customer_password for new password
  4. Toggle UI between login and recover
  5. Generic errors for security
  6. return_to for redirect control
  7. 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...