Customer Account Pages Intermediate 10 min read

Register Page: Schema and Structure

Build a complete customer registration page with form handling, validation, error messages, and customizable sections.

The registration page is often a customer’s first interaction with your account system. A well-designed registration form builds trust and reduces friction.

Basic Registration Template

{# templates/customers/register.liquid #}
{% section 'customer-register' %}

Registration Section

{# sections/customer-register.liquid #}
<section class="customer-register section-{{ section.id }}">
<div class="container container--narrow">
<header class="customer-register__header">
<h1 class="customer-register__heading">{{ section.settings.heading }}</h1>
{%- if section.settings.subheading != blank -%}
<p class="customer-register__subheading">{{ section.settings.subheading }}</p>
{%- endif -%}
</header>
{%- form 'create_customer', class: 'customer-register__form' -%}
{# Error handling #}
{%- if form.errors -%}
<div class="customer-register__errors" role="alert">
<h2 class="visually-hidden">Form errors</h2>
<ul class="customer-register__error-list">
{%- for field in form.errors -%}
<li>
{%- if field == 'form' -%}
{{ form.errors.messages[field] }}
{%- else -%}
{{ form.errors.translated_fields[field] }}: {{ form.errors.messages[field] }}
{%- endif -%}
</li>
{%- endfor -%}
</ul>
</div>
{%- endif -%}
<div class="customer-register__fields">
{# First name #}
<div class="customer-register__field">
<label for="register-first-name">First name</label>
<input
type="text"
id="register-first-name"
name="customer[first_name]"
value="{{ form.first_name }}"
autocomplete="given-name"
required
>
</div>
{# Last name #}
<div class="customer-register__field">
<label for="register-last-name">Last name</label>
<input
type="text"
id="register-last-name"
name="customer[last_name]"
value="{{ form.last_name }}"
autocomplete="family-name"
required
>
</div>
{# Email #}
<div class="customer-register__field customer-register__field--full">
<label for="register-email">Email</label>
<input
type="email"
id="register-email"
name="customer[email]"
value="{{ form.email }}"
autocomplete="email"
required
aria-describedby="email-help"
>
<span id="email-help" class="customer-register__help">
We'll send order confirmations to this address.
</span>
</div>
{# Password #}
<div class="customer-register__field customer-register__field--full">
<label for="register-password">Password</label>
<input
type="password"
id="register-password"
name="customer[password]"
autocomplete="new-password"
required
minlength="5"
aria-describedby="password-help"
>
<span id="password-help" class="customer-register__help">
Minimum 5 characters
</span>
</div>
</div>
{# Marketing opt-in #}
{%- if section.settings.show_marketing_checkbox -%}
<div class="customer-register__marketing">
<label class="customer-register__checkbox-label">
<input
type="checkbox"
name="customer[accepts_marketing]"
value="true"
{% if section.settings.marketing_default_checked %}checked{% endif %}
>
<span>{{ section.settings.marketing_text }}</span>
</label>
</div>
{%- endif -%}
<button type="submit" class="customer-register__submit button button--primary">
{{ section.settings.button_text }}
</button>
<div class="customer-register__footer">
<p>
Already have an account?
<a href="{{ routes.account_login_url }}">Log in</a>
</p>
</div>
{%- endform -%}
</div>
</section>
{% schema %}
{
"name": "Customer Register",
"settings": [
{
"type": "header",
"content": "Content"
},
{
"type": "text",
"id": "heading",
"label": "Heading",
"default": "Create Account"
},
{
"type": "text",
"id": "subheading",
"label": "Subheading",
"default": "Join us to track orders and save your favorites."
},
{
"type": "text",
"id": "button_text",
"label": "Button text",
"default": "Create Account"
},
{
"type": "header",
"content": "Marketing"
},
{
"type": "checkbox",
"id": "show_marketing_checkbox",
"label": "Show marketing opt-in",
"default": true
},
{
"type": "text",
"id": "marketing_text",
"label": "Marketing checkbox text",
"default": "Subscribe to our newsletter for exclusive offers"
},
{
"type": "checkbox",
"id": "marketing_default_checked",
"label": "Checked by default",
"default": false,
"info": "Consider GDPR compliance in your region"
}
]
}
{% endschema %}

Enhanced Error Handling

Provide clear, field-specific error feedback:

{# Enhanced field with error state #}
<div class="customer-register__field{% if form.errors contains 'email' %} customer-register__field--error{% endif %}">
<label for="register-email">
Email
{%- if form.errors contains 'email' -%}
<span class="customer-register__error-icon" aria-hidden="true">!</span>
{%- endif -%}
</label>
<input
type="email"
id="register-email"
name="customer[email]"
value="{{ form.email }}"
autocomplete="email"
required
{% if form.errors contains 'email' %}
aria-invalid="true"
aria-describedby="email-error"
{% endif %}
>
{%- if form.errors contains 'email' -%}
<span id="email-error" class="customer-register__field-error" role="alert">
{{ form.errors.messages.email }}
</span>
{%- endif -%}
</div>

Password Visibility Toggle

Add a show/hide password feature:

<div class="customer-register__field customer-register__field--password">
<label for="register-password">Password</label>
<div class="password-field">
<input
type="password"
id="register-password"
name="customer[password]"
autocomplete="new-password"
required
minlength="5"
>
<button
type="button"
class="password-toggle"
aria-label="Show password"
data-password-toggle
>
<span class="password-toggle__show">Show</span>
<span class="password-toggle__hide" hidden>Hide</span>
</button>
</div>
</div>
// Password toggle functionality
document.querySelectorAll('[data-password-toggle]').forEach(button => {
button.addEventListener('click', () => {
const input = button.previousElementSibling;
const isPassword = input.type === 'password';
input.type = isPassword ? 'text' : 'password';
button.querySelector('.password-toggle__show').hidden = isPassword;
button.querySelector('.password-toggle__hide').hidden = !isPassword;
button.setAttribute('aria-label', isPassword ? 'Hide password' : 'Show password');
});
});

Phone Number Field (Optional)

{# Optional phone field #}
{%- if section.settings.show_phone -%}
<div class="customer-register__field customer-register__field--full">
<label for="register-phone">
Phone
{%- unless section.settings.phone_required -%}
<span class="customer-register__optional">(optional)</span>
{%- endunless -%}
</label>
<input
type="tel"
id="register-phone"
name="customer[phone]"
value="{{ form.phone }}"
autocomplete="tel"
{% if section.settings.phone_required %}required{% endif %}
>
</div>
{%- endif -%}

Registration Benefits

Highlight why customers should register:

{%- if section.blocks.size > 0 -%}
<div class="customer-register__benefits">
<h2 class="customer-register__benefits-heading">
{{ section.settings.benefits_heading }}
</h2>
<ul class="customer-register__benefits-list">
{%- for block in section.blocks -%}
<li class="customer-register__benefit" {{ block.shopify_attributes }}>
{%- if block.settings.icon != blank -%}
<span class="customer-register__benefit-icon">
{{ block.settings.icon }}
</span>
{%- endif -%}
<span>{{ block.settings.text }}</span>
</li>
{%- endfor -%}
</ul>
</div>
{%- endif -%}

Add to schema:

"blocks": [
{
"type": "benefit",
"name": "Benefit",
"settings": [
{
"type": "text",
"id": "icon",
"label": "Icon (emoji or text)",
"default": "✓"
},
{
"type": "text",
"id": "text",
"label": "Benefit text",
"default": "Track your orders"
}
]
}
]

Registration Styles

.customer-register {
padding: var(--spacing-2xl) 0;
}
.customer-register__header {
text-align: center;
margin-bottom: var(--spacing-xl);
}
.customer-register__heading {
font-size: clamp(1.75rem, 4vw, 2.5rem);
margin-bottom: var(--spacing-sm);
}
.customer-register__subheading {
color: var(--color-text-light);
font-size: 1.125rem;
}
.customer-register__fields {
display: grid;
gap: var(--spacing-md);
}
@media (min-width: 480px) {
.customer-register__fields {
grid-template-columns: 1fr 1fr;
}
.customer-register__field--full {
grid-column: span 2;
}
}
.customer-register__field label {
display: block;
font-size: 0.875rem;
font-weight: 500;
margin-bottom: var(--spacing-xs);
}
.customer-register__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-register__field input:focus {
outline: none;
border-color: var(--color-primary);
box-shadow: 0 0 0 3px rgba(var(--color-primary-rgb), 0.1);
}
.customer-register__field--error input {
border-color: var(--color-error);
}
.customer-register__help {
display: block;
font-size: 0.75rem;
color: var(--color-text-light);
margin-top: var(--spacing-xs);
}
.customer-register__field-error {
display: block;
font-size: 0.75rem;
color: var(--color-error);
margin-top: var(--spacing-xs);
}
.customer-register__errors {
background: #fef2f2;
border: 1px solid #fecaca;
border-radius: var(--border-radius);
padding: var(--spacing-md);
margin-bottom: var(--spacing-lg);
}
.customer-register__error-list {
margin: 0;
padding-left: var(--spacing-md);
color: #b91c1c;
}
.customer-register__marketing {
margin: var(--spacing-lg) 0;
}
.customer-register__checkbox-label {
display: flex;
gap: var(--spacing-sm);
align-items: flex-start;
cursor: pointer;
}
.customer-register__checkbox-label input {
margin-top: 0.25em;
}
.customer-register__submit {
width: 100%;
margin-top: var(--spacing-md);
}
.customer-register__footer {
text-align: center;
margin-top: var(--spacing-lg);
padding-top: var(--spacing-lg);
border-top: 1px solid var(--color-border);
}
/* Password field */
.password-field {
position: relative;
}
.password-toggle {
position: absolute;
right: var(--spacing-sm);
top: 50%;
transform: translateY(-50%);
background: none;
border: none;
color: var(--color-text-light);
cursor: pointer;
font-size: 0.75rem;
}
/* Benefits */
.customer-register__benefits {
margin-top: var(--spacing-2xl);
padding-top: var(--spacing-xl);
border-top: 1px solid var(--color-border);
}
.customer-register__benefits-heading {
font-size: 1.25rem;
margin-bottom: var(--spacing-md);
text-align: center;
}
.customer-register__benefits-list {
list-style: none;
padding: 0;
margin: 0;
display: grid;
gap: var(--spacing-sm);
}
.customer-register__benefit {
display: flex;
gap: var(--spacing-sm);
align-items: center;
}
.customer-register__benefit-icon {
flex-shrink: 0;
color: var(--color-success);
}

Form Validation

Add client-side validation:

// Register form validation
const registerForm = document.querySelector('.customer-register__form');
if (registerForm) {
registerForm.addEventListener('submit', (e) => {
const password = registerForm.querySelector('[name="customer[password]"]');
// Check password length
if (password.value.length < 5) {
e.preventDefault();
password.setCustomValidity('Password must be at least 5 characters');
password.reportValidity();
} else {
password.setCustomValidity('');
}
});
}

Practice Exercise

Build a complete registration page with:

  1. First name, last name, email, and password fields
  2. Error handling with field-specific messages
  3. Password visibility toggle
  4. Marketing opt-in checkbox
  5. Benefits list using blocks

Test by:

  • Submitting with invalid data
  • Checking error message display
  • Verifying successful registration redirects to account

Key Takeaways

  1. create_customer form handles registration
  2. form.errors provides validation messages
  3. Autocomplete attributes improve UX
  4. Marketing opt-in with accepts_marketing
  5. ARIA attributes for accessibility
  6. Password requirements enforced server-side
  7. Schema settings for merchant customization

🛠 Speed Up Schema Creation: Use our Schema Builder to visually create section schemas with settings, blocks, and presets—perfect for building customer account page sections.

What’s Next?

The next lesson covers Login and Forgot Password Flows for returning customers.

Finished this lesson?

Mark it complete to track your progress.

Discussion

Loading comments...