diff --git a/lib/web/components/core_components.ex b/lib/web/components/core_components.ex
index 235e9ee..92d5c24 100644
--- a/lib/web/components/core_components.ex
+++ b/lib/web/components/core_components.ex
@@ -3,4 +3,99 @@ defmodule Web.CoreComponents do
   Provides core UI components.
   """
   use Phoenix.Component
+
+  alias Phoenix.HTML.FormField
+
+  attr :id, :any, default: nil
+  attr :name, :any
+  attr :label, :string, default: nil
+  attr :value, :any
+  attr :type, :string, default: "text", values: ~w[text password]
+  attr :field, FormField
+  attr :errors, :list, default: []
+  attr :rest, :global, include: ~w[disabled form pattern placeholder readonly required]
+
+  def input(%{field: %FormField{} = field} = assigns) do
+    errors =
+      if Phoenix.Component.used_input?(field) do
+        field.errors
+      else
+        []
+      end
+
+    assigns
+    |> assign(field: nil, id: assigns.id || field.id, errors: Enum.map(errors, &translate_error/1))
+    |> assign_new(:name, fn -> field.name end)
+    |> assign_new(:value, fn -> field.value end)
+    |> input()
+  end
+
+  def input(assigns) do
+    ~H"""
+    <div>
+      <.label for={@id}>{@label}</.label>
+      <input
+        id={@id}
+        type={@type}
+        name={@name}
+        value={Phoenix.HTML.Form.normalize_value(@type, @value)}
+        {@rest}
+      />
+      <.error :for={error <- @errors}>{error}</.error>
+    </div>
+    """
+  end
+
+  attr :for, :string, default: nil
+  slot :inner_block, required: true
+
+  def label(assigns) do
+    ~H"""
+    <label for={@for}>
+      {render_slot(@inner_block)}
+    </label>
+    """
+  end
+
+  slot :inner_block, required: true
+
+  def error(assigns) do
+    ~H"""
+    <p>
+      <.icon name="hero-exclamation-circle-mini" class="h-5 w-5 flex-none" />
+      {render_slot(@inner_block)}
+    </p>
+    """
+  end
+
+  attr :name, :string, required: true
+  attr :class, :string, default: nil
+
+  def icon(%{name: "hero-" <> _} = assigns) do
+    ~H"""
+    <span class={[@name, @class]} />
+    """
+  end
+
+  def translate_error({msg, opts}) do
+    Enum.reduce(opts, msg, fn {key, value}, acc ->
+      String.replace(acc, "%{#{key}}", fn _ -> to_string(value) end)
+    end)
+  end
+
+  def translate_errors(errors, field) when is_list(errors) do
+    for {^field, {msg, opts}} <- errors, do: translate_error({msg, opts})
+  end
+
+  attr :type, :string, default: "button", values: ~w[button submit]
+  attr :rest, :global
+  slot :inner_block, required: true
+
+  def button(assigns) do
+    ~H"""
+    <button type={@type} {@rest}>
+      {render_slot(@inner_block)}
+    </button>
+    """
+  end
 end
diff --git a/lib/web/live/user_login_live.ex b/lib/web/live/user_login_live.ex
index 3dcf3ac..3381c3f 100644
--- a/lib/web/live/user_login_live.ex
+++ b/lib/web/live/user_login_live.ex
@@ -2,46 +2,24 @@ defmodule Web.UserLoginLive do
   @moduledoc false
   use Web, :live_view
 
+  def mount(_params, _session, socket) do
+    form = to_form(%{}, as: "user")
+    {:ok, assign(socket, form: form), temporary_assigns: [form: form], layout: false}
+  end
+
   def render(assigns) do
-    # ~H"""
-    # <div class="mx-auto max-w-sm">
-    #   <.header class="text-center">
-    #     Log in to account
-    #     <:subtitle>
-    #       Don't have an account?
-    #       <.link navigate={~p/admin/users/register"} class="font-semibold text-brand hover:underline">
-    #         Sign up
-    #       </.link>
-    #       for an account now.
-    #     </:subtitle>
-    #   </.header>
-
-    #   <.simple_form for={@form} id="login_form" action={~p/admin/users/log_in"} phx-update="ignore">
-    #     <.input field={@form[:email]} type="email" label="Email" required />
-    #     <.input field={@form[:password]} type="password" label="Password" required />
-
-    #     <:actions>
-    #       <.input field={@form[:remember_me]} type="checkbox" label="Keep me logged in" />
-    #       <.link href={~p/admin/users/reset_password"} class="text-sm font-semibold">
-    #         Forgot your password?
-    #       </.link>
-    #     </:actions>
-    #     <:actions>
-    #       <.button phx-disable-with="Logging in..." class="w-full">
-    #         Log in <span aria-hidden="true">→</span>
-    #       </.button>
-    #     </:actions>
-    #   </.simple_form>
-    # </div>
-    # """
     ~H"""
-    <pre>UserLoginLive</pre>
+    <div class="mx-auto max-w-sm">
+      <header>sign in</header>
+
+      <.form for={@form} id="login_form" action={~p"/admin/users/log_in"} phx-update="ignore">
+        <.input field={@form[:username]} type="text" label="username" required />
+        <.input field={@form[:password]} type="password" label="password" required />
+        <.button phx-disable-with="signing in..." class="w-full" type="submit">
+          sign in
+        </.button>
+      </.form>
+    </div>
     """
   end
-
-  def mount(_params, _session, socket) do
-    email = Phoenix.Flash.get(socket.assigns.flash, :email)
-    form = to_form(%{"email" => email}, as: "user")
-    {:ok, assign(socket, form: form), temporary_assigns: [form: form]}
-  end
 end
diff --git a/lib/web/live/user_registration_live.ex b/lib/web/live/user_registration_live.ex
index 880aaa1..41e6e83 100644
--- a/lib/web/live/user_registration_live.ex
+++ b/lib/web/live/user_registration_live.ex
@@ -4,67 +4,29 @@ defmodule Web.UserRegistrationLive do
 
   alias Core.Accounts
 
-  def render(assigns) do
-    # ~H"""
-    # <div class="mx-auto max-w-sm">
-    #   <.header class="text-center">
-    #     Register for an account
-    #     <:subtitle>
-    #       Already registered?
-    #       <.link navigate={~p/admin/users/log_in"} class="font-semibold text-brand hover:underline">
-    #         Log in
-    #       </.link>
-    #       to your account now.
-    #     </:subtitle>
-    #   </.header>
-
-    #   <.simple_form
-    #     for={@form}
-    #     id="registration_form"
-    #     phx-submit="save"
-    #     phx-change="validate"
-    #     phx-trigger-action={@trigger_submit}
-    #     action={~p/admin/users/log_in?_action=registered"}
-    #     method="post"
-    #   >
-    #     <.error :if={@check_errors}>
-    #       Oops, something went wrong! Please check the errors below.
-    #     </.error>
-
-    #     <.input field={@form[:email]} type="email" label="Email" required />
-    #     <.input field={@form[:password]} type="password" label="Password" required />
-
-    #     <:actions>
-    #       <.button phx-disable-with="Creating account..." class="w-full">Create an account</.button>
-    #     </:actions>
-    #   </.simple_form>
-    # </div>
-    # """
-    ~H"""
-    <pre>UserRegistrationLive</pre>
-    """
-  end
-
   def mount(_params, _session, socket) do
     changeset = Accounts.change_user_registration(%Schema.User{})
 
     socket =
       socket
-      |> assign(trigger_submit: false, check_errors: false)
+      |> assign(trigger_submit: false)
       |> assign_form(changeset)
 
-    {:ok, socket, temporary_assigns: [form: nil]}
+    {:ok, socket, temporary_assigns: [form: nil], layout: false}
   end
 
   def handle_event("save", %{"user" => user_params}, socket) do
-    case Accounts.register_user(user_params) do
-      {:ok, user} ->
-        changeset = Accounts.change_user_registration(user)
-        {:noreply, socket |> assign(trigger_submit: true) |> assign_form(changeset)}
+    socket =
+      case Accounts.register_user(user_params) do
+        {:ok, user} ->
+          changeset = Accounts.change_user_registration(user)
+          socket |> assign(trigger_submit: true) |> assign_form(changeset)
 
-      {:error, %Ecto.Changeset{} = changeset} ->
-        {:noreply, socket |> assign(check_errors: true) |> assign_form(changeset)}
-    end
+        {:error, %Ecto.Changeset{} = changeset} ->
+          assign_form(socket, changeset)
+      end
+
+    {:noreply, socket}
   end
 
   def handle_event("validate", %{"user" => user_params}, socket) do
@@ -72,13 +34,31 @@ defmodule Web.UserRegistrationLive do
     {:noreply, assign_form(socket, Map.put(changeset, :action, :validate))}
   end
 
+  def render(assigns) do
+    ~H"""
+    <div class="mx-auto max-w-sm">
+      <header class="text-center">finish installation</header>
+
+      <.form
+        for={@form}
+        id="registration_form"
+        phx-submit="save"
+        phx-change="validate"
+        phx-trigger-action={@trigger_submit}
+        action={~p"/admin/users/log_in?_action=registered"}
+        method="post"
+      >
+        <.input field={@form[:username]} type="text" label="username" />
+        <.input field={@form[:password]} type="password" label="password" />
+        <.button type="submit">create administrator</.button>
+      </.form>
+    </div>
+    """
+  end
+
   defp assign_form(socket, %Ecto.Changeset{} = changeset) do
     form = to_form(changeset, as: "user")
 
-    if changeset.valid? do
-      assign(socket, form: form, check_errors: false)
-    else
-      assign(socket, form: form)
-    end
+    assign(socket, form: form)
   end
 end