diff --git a/assets/js/_form-validity.js b/assets/js/_form-validity.js
new file mode 100644
index 0000000..d897996
--- /dev/null
+++ b/assets/js/_form-validity.js
@@ -0,0 +1,18 @@
+// Fetch all the forms we want to apply custom Bootstrap validation styles to
+let forms = document.querySelectorAll(".needs-validation");
+
+// Loop over them and prevent submission
+Array.prototype.slice.call(forms).forEach(function (form) {
+  form.addEventListener(
+    "submit",
+    function (event) {
+      if (!form.checkValidity()) {
+        event.preventDefault();
+        event.stopPropagation();
+      }
+
+      form.classList.add("was-validated");
+    },
+    false
+  );
+});
diff --git a/assets/js/app.js b/assets/js/app.js
index 7ebb229..1b904f1 100644
--- a/assets/js/app.js
+++ b/assets/js/app.js
@@ -15,6 +15,7 @@ import "../node_modules/bootstrap-icons/icons/box-arrow-right.svg"; // log out
 import "../node_modules/bootstrap-icons/icons/at.svg";
 import "../node_modules/bootstrap-icons/icons/key.svg";
 import "../node_modules/bootstrap-icons/icons/key-fill.svg";
+import "../node_modules/bootstrap-icons/icons/envelope.svg";
 
 // webpack automatically bundles all modules in your
 // entry points. Those entry points can be configured
@@ -36,6 +37,7 @@ import "bootstrap/js/dist/dropdown";
 import "bootstrap/js/dist/alert";
 // Boostrap helpers
 import "./_hamburger-helper";
+import "./_form-validity";
 import { AlertRemover } from "./_alert-remover";
 
 // LiveSocket setup
diff --git a/lib/bones73k_web/templates/user_session/new.html.eex b/lib/bones73k_web/templates/user_session/new.html.eex
index 7276e19..4510b65 100644
--- a/lib/bones73k_web/templates/user_session/new.html.eex
+++ b/lib/bones73k_web/templates/user_session/new.html.eex
@@ -7,7 +7,7 @@
   </h3>
   <p class="lead">Who goes there?</p>
 
-  <%= form_for @conn, Routes.user_session_path(@conn, :create), [as: :user], fn f -> %>
+  <%= form_for @conn, Routes.user_session_path(@conn, :create), [as: :user, class: "needs-validation", novalidate: true], fn f -> %>
     <%= if @error_message do %>
       <div class="alert alert-danger alert-dismissible fade show" role="alert">
         <%= @error_message %>
@@ -22,7 +22,11 @@
       </span>
       <%= email_input f, :email,
           class: "form-control",
-          required: true %>
+          placeholder: "e.g., babka@73k.us",
+          maxlength: User.max_email,
+          required: true
+        %>
+      <span class="invalid-feedback">must be a valid email address</span>
     </div>
 
     <%= label f, :password, class: "form-label" %>
@@ -32,7 +36,9 @@
       </span>
       <%= password_input f, :password,
           class: "form-control",
-          required: true %>
+          required: true
+        %>
+      <span class="invalid-feedback">password is required</span>
     </div>
 
     <div class="form-check mb-3">
diff --git a/lib/bones73k_web/views/user_session_view.ex b/lib/bones73k_web/views/user_session_view.ex
index 4f5a1e9..a7f878c 100644
--- a/lib/bones73k_web/views/user_session_view.ex
+++ b/lib/bones73k_web/views/user_session_view.ex
@@ -1,3 +1,4 @@
 defmodule Bones73kWeb.UserSessionView do
   use Bones73kWeb, :view
+  alias Bones73k.Accounts.User
 end