Patterns for building login/signup pages with Netlify Identity — dual views, custom forms, GoTrue JS, controller class
Use tabs to toggle between two views:
Toggle with CSS classes (hidden/visible) controlled by a controller class.
Identity widget does NOT support custom form fields. Use GoTrue JS directly:
const auth = new GoTrue({ APIUrl: `${window.location.origin}/.netlify/identity`, setCookie: true });
await auth.signup(email, password, {
full_name: name,
linkedin_url: url,
company_name: company
});
user_metadata (not visible in Netlify UI, but stored on user)class LoginController {
constructor() {
this.widget = window.netlifyIdentity;
this.auth = new GoTrue({ APIUrl: '...', setCookie: true });
// cache DOM elements
}
init() { this.bindEvents(); this.checkExistingSession(); }
bindEvents() { /* DOM events first, then widget events */ }
checkExistingSession() { /* widget.currentUser() → redirect */ }
showLoginView() { /* toggle tabs + views */ }
showSignupView() { /* toggle tabs + views */ }
handleSignup(e) { /* validate, call auth.signup(), show confirmation */ }
showConfirmation() { /* "check your email" message */ }
openLogin() { this.widget.open('login'); }
redirectToDashboard() { window.location.href = '/'; }
}
Key: Bind DOM event listeners (tabs, form submit) BEFORE widget event listeners. If widget throws, DOM events still work.
Common error: Binding widget events first → if widget throws, tab click handlers never get registered → UI appears broken.
<script src="https://identity.netlify.com/v1/netlify-identity-widget.js"></script>
<script src="https://unpkg.com/gotrue-js@0.9.29/dist/gotrue.js"></script>
<script>
// Initialize inline — both scripts are loaded synchronously above
const controller = new LoginController();
controller.init();
</script>
Common error: Wrapping init in DOMContentLoaded or load — inline execution after synchronous scripts is more reliable.