Transactional journeys
Signup
A finished page under Kiln. Copy it and it is yours outright: unlike a brickwork component or shell, it never upgrades under semver.
Skip to main content
Northwind
Create your account
Free for 30 days. No card needed.
Already have an account? Sign in
By creating an account you agree to our terms of service and privacy policy.
Scroll inside the frame to inspect the full page.
Paste into a static page. Link @bwui/css (and @bwui/js for behaviours).
<!DOCTYPE html>
<html lang="en-GB" data-bw-brand="northline">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Create your account | Northline</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Source+Sans+3:wght@400;500;600;700&family=Source+Serif+4:opsz,wght@8..60,500;8..60,650;8..60,700&display=swap"
rel="stylesheet"
/>
<link rel="stylesheet" href="/static/product_templates/assets/brickwork.css" />
<script defer src="/static/product_templates/assets/behaviours.js"></script>
<style>
/* Northline brand pack (fictional): cool paper, teal accent, display serif.
Colour lives in --bw-* tokens only, so an L1 pack recolours this page. */
:root,
[data-bw-brand="northline"] {
--bw-color-surface: oklch(0.985 0.004 240);
--bw-color-fg: oklch(0.26 0.02 250);
--bw-color-border: oklch(0.90 0.01 240);
--bw-color-accent: oklch(0.42 0.05 230);
--bw-color-danger: oklch(0.55 0.18 25);
--bw-color-success: oklch(0.56 0.13 155);
--bw-color-warning: oklch(0.70 0.14 75);
--bw-color-fg-on-accent: oklch(0.99 0 0);
--bw-font-family-sans: "Source Sans 3", system-ui, sans-serif;
--bw-font-family-display: "Source Serif 4", Georgia, serif;
}
body {
margin: 0;
font-family: var(--bw-font-family-sans);
background: var(--bw-color-surface);
color: var(--bw-color-fg);
}
.nl-auth {
display: grid;
min-block-size: 100vh;
}
@media (min-width: 62rem) {
.nl-auth {
grid-template-columns: minmax(0, 5fr) minmax(0, 6fr);
}
}
/* Brand panel: built from the accent token, so it recolours with the brand. */
.nl-aside {
position: relative;
display: none;
flex-direction: column;
justify-content: space-between;
gap: var(--bw-space-10);
padding: clamp(2rem, 4vw, 3.5rem);
color: var(--bw-color-fg-on-accent);
overflow: hidden;
background:
linear-gradient(165deg,
color-mix(in oklab, var(--bw-color-accent) 45%, black) 0%,
color-mix(in oklab, var(--bw-color-accent) 85%, black) 55%,
var(--bw-color-accent) 100%);
}
@media (min-width: 62rem) {
.nl-aside {
display: flex;
}
}
.nl-aside::after {
content: "";
position: absolute;
inset: auto -20% -35% 15%;
block-size: 80%;
background:
radial-gradient(ellipse at 40% 40%, color-mix(in oklab, white 16%, transparent) 0%, transparent 60%);
pointer-events: none;
}
.nl-aside > * {
position: relative;
z-index: 1;
}
.nl-brand {
display: inline-flex;
align-items: center;
gap: var(--bw-space-2);
font-weight: 700;
font-size: 1.125rem;
letter-spacing: -0.03em;
color: inherit;
text-decoration: none;
}
.nl-brand__mark {
inline-size: 1.125rem;
block-size: 1.125rem;
border-radius: 5px;
background: currentColor;
}
.nl-aside__heading {
margin: 0 0 var(--bw-space-6);
max-inline-size: 14ch;
font-family: var(--bw-font-family-display);
font-weight: 650;
font-size: clamp(2rem, 3.4vw, 3rem);
letter-spacing: -0.04em;
line-height: 1.08;
}
.nl-points {
display: grid;
gap: var(--bw-space-4);
margin: 0;
padding: 0;
list-style: none;
}
.nl-points li {
position: relative;
padding-inline-start: 2rem;
max-inline-size: 36ch;
line-height: 1.45;
opacity: 0.88;
}
.nl-points li::before {
content: "";
position: absolute;
inset-block-start: 0.15rem;
inset-inline-start: 0;
inline-size: 1.25rem;
block-size: 1.25rem;
border-radius: 999px;
background: color-mix(in oklab, white 20%, transparent);
}
.nl-points li::after {
content: "";
position: absolute;
inset-block-start: 0.4rem;
inset-inline-start: 0.4rem;
inline-size: 0.45rem;
block-size: 0.25rem;
border-inline-start: 2px solid currentColor;
border-block-end: 2px solid currentColor;
transform: rotate(-45deg);
}
.nl-quote {
margin: 0;
padding-top: var(--bw-space-5);
border-top: 1px solid color-mix(in oklab, white 22%, transparent);
}
.nl-quote blockquote {
margin: 0 0 var(--bw-space-3);
font-family: var(--bw-font-family-display);
font-size: 1.25rem;
line-height: 1.35;
letter-spacing: -0.01em;
}
.nl-quote figcaption {
font-size: var(--bw-font-size-sm);
opacity: 0.75;
}
.nl-main {
display: flex;
flex-direction: column;
padding: clamp(1.5rem, 4vw, 3rem);
}
.nl-main__bar {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--bw-space-4);
font-size: var(--bw-font-size-sm);
color: var(--bw-color-fg-muted);
}
.nl-main__bar .nl-brand {
color: var(--bw-color-fg);
}
@media (min-width: 62rem) {
.nl-main__bar .nl-brand {
visibility: hidden;
}
}
.nl-main__bar a:not(.nl-brand) {
color: var(--bw-color-fg);
font-weight: 600;
text-underline-offset: 0.2em;
}
.nl-card {
inline-size: min(100%, 25rem);
margin: auto;
padding-block: var(--bw-space-8);
}
.nl-card__heading {
margin: 0 0 var(--bw-space-2);
font-family: var(--bw-font-family-display);
font-weight: 650;
font-size: 2rem;
letter-spacing: -0.03em;
line-height: 1.1;
}
.nl-card__sub {
margin: 0 0 var(--bw-space-6);
color: var(--bw-color-fg-muted);
line-height: 1.5;
}
.nl-form .bw-field,
.nl-form .bw-form-errors {
max-inline-size: none;
margin-block-end: var(--bw-space-4);
}
.nl-form .bw-form-errors__list a {
color: inherit;
text-underline-offset: 0.2em;
}
.nl-check {
display: flex;
align-items: flex-start;
gap: var(--bw-space-3);
font-size: var(--bw-font-size-sm);
line-height: 1.45;
}
.nl-check .bw-checkbox {
flex: none;
margin: 0.15rem 0 0;
}
.nl-check a {
color: inherit;
text-underline-offset: 0.2em;
}
.nl-form .bw-field--check {
margin-block-end: var(--bw-space-5);
}
.nl-block {
inline-size: 100%;
}
.nl-switch {
margin: var(--bw-space-6) 0 0;
text-align: center;
color: var(--bw-color-fg-muted);
font-size: var(--bw-font-size-sm);
}
.nl-switch a {
color: var(--bw-color-fg);
font-weight: 600;
text-underline-offset: 0.2em;
}
.nl-legal {
margin: 0;
text-align: center;
color: var(--bw-color-fg-muted);
font-size: var(--bw-font-size-xs);
}
.nl-legal a {
color: inherit;
}
</style>
</head>
<body data-theme="light" data-bw-brand="northline">
<a class="bw-skip-link" href="#main">Skip to content</a>
<div class="nl-auth">
<aside class="nl-aside" aria-label="About Northline">
<a class="nl-brand" href="/templates/homepage/full/">
<span class="nl-brand__mark" aria-hidden="true"></span>
Northline
</a>
<div>
<p class="nl-aside__heading">One board for every craft on the road.</p>
<ul class="nl-points">
<li>Live ETAs your dispatchers and drivers both trust.</li>
<li>Handoffs that take 6 minutes, not a phone round.</li>
<li>Invite your depot team in minutes and set roles as you go.</li>
</ul>
</div>
<figure class="nl-quote">
<blockquote>We stopped rebuilding the ops screen every quarter.</blockquote>
<figcaption>Priya Raman, Head of Operations, Harbour Freight</figcaption>
</figure>
</aside>
<main id="main" class="nl-main">
<div class="nl-main__bar">
<a class="nl-brand" href="/templates/homepage/full/">
<span class="nl-brand__mark" aria-hidden="true"></span>
Northline
</a>
<span>Already have an account? <a href="/templates/auth-signin/full/">Sign in</a></span>
</div>
<div class="nl-card">
<h1 class="nl-card__heading">Create your account</h1>
<p class="nl-card__sub">Free for 14 days. No card needed, and your first depot board is ready in minutes.</p>
<form class="nl-form" action="#" method="post" novalidate data-nl-form>
<div class="bw-form-errors" role="alert" data-nl-summary>
<p class="bw-form-errors__title" data-nl-summary-title>Check 2 fields before continuing</p>
<ul class="bw-form-errors__list">
<li data-for="email"><a href="#email">Enter a work email in the form name@company.com.</a></li>
<li data-for="password"><a href="#password">Use at least 12 characters for your password.</a></li>
</ul>
</div>
<div class="bw-field">
<label class="bw-field__label" for="name">Full name</label>
<input class="bw-input" id="name" name="name" autocomplete="name" value="Priya Raman" required aria-describedby="name-error" data-msg-required="Enter your name." />
<p class="bw-field__error" id="name-error" hidden></p>
</div>
<div class="bw-field bw-field--invalid">
<label class="bw-field__label" for="email">Work email</label>
<input
class="bw-input"
id="email"
name="email"
type="email"
autocomplete="email"
value="priya.raman@harbour"
required
aria-invalid="true"
aria-describedby="email-error"
data-msg-required="Enter your work email."
data-msg-format="Enter a work email in the form name@company.com."
/>
<p class="bw-field__error" id="email-error">Enter a work email in the form name@company.com.</p>
</div>
<div class="bw-field bw-field--invalid">
<label class="bw-field__label" for="password">Password</label>
<div class="bw-password">
<input
class="bw-input"
id="password"
name="password"
type="password"
autocomplete="new-password"
minlength="12"
value="harbour2026"
required
aria-invalid="true"
aria-describedby="password-hint password-error"
data-msg-required="Choose a password."
data-msg-short="Use at least 12 characters for your password."
/>
<button type="button" class="bw-btn bw-btn--ghost bw-btn--sm bw-password__toggle" aria-pressed="false">Show</button>
</div>
<p class="bw-field__hint" id="password-hint">At least 12 characters. A short phrase works well.</p>
<p class="bw-field__error" id="password-error">Use at least 12 characters for your password.</p>
</div>
<div class="bw-field bw-field--check">
<label class="nl-check">
<input class="bw-checkbox" id="terms" type="checkbox" name="terms" required aria-describedby="terms-error" data-msg-required="Accept the terms to create your account." />
<span>I agree to the <a href="/templates/legal/full/">terms of service</a> and <a href="/templates/legal/full/">privacy notice</a>.</span>
</label>
<p class="bw-field__error" id="terms-error" hidden></p>
</div>
<button class="bw-btn bw-btn--primary bw-btn--lg nl-block" type="submit">Create account</button>
</form>
<p class="nl-switch">Already have an account? <a href="/templates/auth-signin/full/">Sign in</a></p>
</div>
<p class="nl-legal">
Setting up for a whole team? <a href="/templates/contact/full/">Book a walkthrough</a>
</p>
</main>
</div>
<script>
/* Inline form feedback: re-checks a field as it is edited and keeps the
summary in step. Server-rendered errors use the same markup. */
(function () {
var form = document.querySelector("form[data-nl-form]");
if (!form) return;
var summary = form.querySelector("[data-nl-summary]");
var title = form.querySelector("[data-nl-summary-title]");
var EMAIL = /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/;
var inputs = Array.prototype.slice.call(form.querySelectorAll("[data-msg-required]"));
function problem(input) {
var value = input.type === "checkbox" ? input.checked : input.value.trim();
if (input.required && !value) return "required";
if (input.type === "email" && value && !EMAIL.test(value)) return "format";
if (input.minLength > 0 && value && input.value.length < input.minLength) return "short";
return "";
}
function paint(input, kind) {
var field = input.closest(".bw-field");
var error = field.querySelector(".bw-field__error");
var item = summary && summary.querySelector('[data-for="' + input.id + '"]');
var message = kind ? input.getAttribute("data-msg-" + kind) : "";
error.textContent = message;
error.hidden = !kind;
field.classList.toggle("bw-field--invalid", !!kind);
if (kind) input.setAttribute("aria-invalid", "true");
else input.removeAttribute("aria-invalid");
if (item) {
item.hidden = !kind;
if (kind) item.firstElementChild.textContent = message;
}
}
function sync() {
if (!summary) return;
var count = form.querySelectorAll('[aria-invalid="true"]').length;
summary.hidden = count === 0;
title.textContent = "Check " + count + (count === 1 ? " field" : " fields") + " before continuing";
}
inputs.forEach(function (input) {
var evt = input.type === "checkbox" ? "change" : "input";
input.addEventListener(evt, function () {
if (input.getAttribute("aria-invalid") === "true") {
paint(input, problem(input));
sync();
}
});
input.addEventListener("blur", function () {
if (input.type !== "checkbox" && input.value.trim() !== "") {
paint(input, problem(input));
sync();
}
});
});
form.addEventListener("submit", function (event) {
var first = null;
inputs.forEach(function (input) {
var kind = problem(input);
paint(input, kind);
if (kind && !first) first = input;
});
sync();
if (first) {
event.preventDefault();
first.focus();
}
});
})();
</script>
</body>
</html>
Copy into your project. This example is not on the template loader path.
{% extends "brickwork/shell/auth.html" %}
{% comment %}
Create an account.
COPY THIS FILE into your project and edit it. It is not on the template loader
path, so you cannot extend it (ADR-056).
What your view must supply:
form your signup form
As with signin.html, brickwork names no field and ships no view or URL: allauth
uses email/password1/password2, django.contrib.auth's UserCreationForm uses
username, and bw_form renders either without this file knowing the difference.
The URL below is a PLACEHOLDER; replace it with your own.
States: the form's own valid/invalid states (bw_form renders errors
inline).
Accessibility: inherits shell/auth.html's skip link, lang/dir/theme
attributes and themed bare-prose-link colour; the page's own <h1> names
the task; the terms line is plain text near the submit, never a
pre-ticked checkbox. Covered by the archetype harness's full gate sweep
(render, axe WCAG 2.2 AA, no horizontal overflow, light/dark
distinctness, skip-link first-tab-stop with JS disabled) at every W0.1
breakpoint, both themes; unlike auth/signin.html, this archetype has no
additional hand-maintained a11y fixture of its own, only the archetype
harness's coverage.
Responsive: no breakpoint switch of its own; inherits shell/auth.html's
fluid, no-breakpoint centred panel.
{% endcomment %}
{% load brickwork_components brickwork_forms %}
{% block page_title %}Create your account - Northwind{% endblock %}
{% block brand_wordmark %}Northwind{% endblock %}
{% block content %}
<div class="bw-section-stack">
<h1>Create your account</h1>
<p>Free for 30 days. No card needed.</p>
<form method="post" action="/accounts/signup/">
{% csrf_token %}
{% bw_form form %}
<div class="bw-form__actions">
{% bw_button "Create account" type="submit" variant="primary" %}
</div>
</form>
<p>
Already have an account? <a href="/accounts/login/">Sign in</a>
</p>
{% comment %}
Your terms line, if you have one. Keep it as text near the submit rather
than as a pre-ticked checkbox.
{% endcomment %}
<p>
By creating an account you agree to our
<a href="/terms/">terms of service</a> and
<a href="/privacy/">privacy policy</a>.
</p>
</div>
{% endblock %}
Details
| Kind | Page example |
|---|---|
| Used in | Transactional journeys |
Composed from
| Template | Description |
|---|---|
brickwork/components/_button.html |
A button or link styled as a button, in several variants. |
brickwork/forms/_form.html |
Not a catalogue component |
brickwork/shell/auth.html |
The centred single-column shell for login and similar flows. |