diff --git a/web/src/lib/components/forms/create-user-form.svelte b/web/src/lib/components/forms/create-user-form.svelte
index 7aa1c76ed3..8bd955734a 100644
--- a/web/src/lib/components/forms/create-user-form.svelte
+++ b/web/src/lib/components/forms/create-user-form.svelte
@@ -5,10 +5,8 @@
   import { ByteUnit, convertToBytes } from '$lib/utils/byte-units';
   import { handleError } from '$lib/utils/handle-error';
   import { createUserAdmin } from '@immich/sdk';
+  import { Alert, Button, Field, HelperText, Input, PasswordInput, Stack, Switch } from '@immich/ui';
   import { t } from 'svelte-i18n';
-  import Button from '../elements/buttons/button.svelte';
-  import Slider from '../elements/slider.svelte';
-  import PasswordField from '../shared-components/password-field.svelte';
 
   interface Props {
     onClose: () => void;
@@ -17,137 +15,114 @@
     oauthEnabled?: boolean;
   }
 
-  let { onClose, onSubmit, onCancel, oauthEnabled = false }: Props = $props();
+  let { onClose, onSubmit: onDone, onCancel, oauthEnabled = false }: Props = $props();
 
   let error = $state('');
-  let success = $state('');
+  let success = $state(false);
 
   let email = $state('');
   let password = $state('');
-  let confirmPassword = $state('');
+  let passwordConfirm = $state('');
   let name = $state('');
   let shouldChangePassword = $state(true);
   let notify = $state(true);
 
-  let canCreateUser = $state(false);
-  let quotaSize: number | undefined = $state();
+  let quotaSize: string | undefined = $state();
   let isCreatingUser = $state(false);
 
-  let quotaSizeInBytes = $derived(quotaSize ? convertToBytes(quotaSize, ByteUnit.GiB) : null);
+  let quotaSizeInBytes = $derived(quotaSize ? convertToBytes(Number(quotaSize), ByteUnit.GiB) : null);
   let quotaSizeWarning = $derived(
     quotaSizeInBytes && userInteraction.serverInfo && quotaSizeInBytes > userInteraction.serverInfo.diskSizeRaw,
   );
 
-  $effect(() => {
-    if (password !== confirmPassword && confirmPassword.length > 0) {
-      error = $t('password_does_not_match');
-      canCreateUser = false;
-    } else {
-      error = '';
-      canCreateUser = true;
-    }
-  });
+  const passwordMismatch = $derived(password !== passwordConfirm && passwordConfirm.length > 0);
+  const passwordMismatchMessage = $derived(passwordMismatch ? $t('password_does_not_match') : '');
+  const valid = $derived(!passwordMismatch && !isCreatingUser);
 
-  async function registerUser() {
-    if (canCreateUser && !isCreatingUser) {
-      isCreatingUser = true;
-      error = '';
-
-      try {
-        await createUserAdmin({
-          userAdminCreateDto: {
-            email,
-            password,
-            shouldChangePassword,
-            name,
-            quotaSizeInBytes,
-            notify,
-          },
-        });
-
-        success = $t('new_user_created');
-
-        onSubmit();
-
-        return;
-      } catch (error) {
-        handleError(error, $t('errors.unable_to_create_user'));
-      } finally {
-        isCreatingUser = false;
-      }
-    }
-  }
-
-  const onsubmit = async (event: Event) => {
+  const onSubmit = async (event: Event) => {
     event.preventDefault();
-    await registerUser();
+
+    if (!valid) {
+      return;
+    }
+
+    isCreatingUser = true;
+    error = '';
+
+    try {
+      await createUserAdmin({
+        userAdminCreateDto: {
+          email,
+          password,
+          shouldChangePassword,
+          name,
+          quotaSizeInBytes,
+          notify,
+        },
+      });
+
+      success = true;
+
+      onDone();
+
+      return;
+    } catch (error) {
+      handleError(error, $t('errors.unable_to_create_user'));
+    } finally {
+      isCreatingUser = false;
+    }
   };
 </script>
 
-<FullScreenModal title={$t('create_new_user')} showLogo {onClose}>
-  <form {onsubmit} autocomplete="off" id="create-new-user-form">
-    <div class="my-4 flex flex-col gap-2">
-      <label class="immich-form-label" for="email">{$t('email')}</label>
-      <input class="immich-form-input" id="email" bind:value={email} type="email" required />
-    </div>
-
-    {#if $featureFlags.email}
-      <div class="my-4 flex place-items-center justify-between gap-2">
-        <label class="text-sm dark:text-immich-dark-fg" for="send-welcome-email">
-          {$t('admin.send_welcome_email')}
-        </label>
-        <Slider id="send-welcome-email" bind:checked={notify} />
-      </div>
-    {/if}
-
-    <div class="my-4 flex flex-col gap-2">
-      <label class="immich-form-label" for="password">{$t('password')}</label>
-      <PasswordField id="password" bind:password autocomplete="new-password" required={!oauthEnabled} />
-    </div>
-
-    <div class="my-4 flex flex-col gap-2">
-      <label class="immich-form-label" for="confirmPassword">{$t('confirm_password')}</label>
-      <PasswordField
-        id="confirmPassword"
-        bind:password={confirmPassword}
-        autocomplete="new-password"
-        required={!oauthEnabled}
-      />
-    </div>
-
-    <div class="my-4 flex place-items-center justify-between gap-2">
-      <label class="text-sm dark:text-immich-dark-fg" for="require-password-change">
-        {$t('admin.require_password_change_on_login')}
-      </label>
-      <Slider id="require-password-change" bind:checked={shouldChangePassword} />
-    </div>
-
-    <div class="my-4 flex flex-col gap-2">
-      <label class="immich-form-label" for="name">{$t('name')}</label>
-      <input class="immich-form-input" id="name" bind:value={name} type="text" required />
-    </div>
-
-    <div class="my-4 flex flex-col gap-2">
-      <label class="flex items-center gap-2 immich-form-label" for="quotaSize">
-        {$t('admin.quota_size_gib')}
-        {#if quotaSizeWarning}
-          <p class="text-red-400 text-sm">{$t('errors.quota_higher_than_disk_size')}</p>
-        {/if}
-      </label>
-      <input class="immich-form-input" id="quotaSize" type="number" min="0" bind:value={quotaSize} />
-    </div>
-
+<form onsubmit={onSubmit} autocomplete="off" id="create-new-user-form">
+  <FullScreenModal title={$t('create_new_user')} showLogo {onClose}>
     {#if error}
-      <p class="text-sm text-red-400">{error}</p>
+      <Alert color="danger" size="small" title={error} closable />
     {/if}
 
     {#if success}
-      <p class="text-sm text-immich-primary">{success}</p>
+      <p class="text-sm text-immich-primary">{$t('new_user_created')}</p>
     {/if}
-  </form>
 
-  {#snippet stickyBottom()}
-    <Button color="gray" fullwidth onclick={onCancel}>{$t('cancel')}</Button>
-    <Button type="submit" disabled={isCreatingUser} fullwidth form="create-new-user-form">{$t('create')}</Button>
-  {/snippet}
-</FullScreenModal>
+    <Stack gap={4}>
+      <Field label={$t('email')} required>
+        <Input bind:value={email} type="email" />
+      </Field>
+
+      {#if $featureFlags.email}
+        <Field label={$t('admin.send_welcome_email')}>
+          <Switch id="send-welcome-email" bind:checked={notify} class="flex justify-between text-sm" />
+        </Field>
+      {/if}
+
+      <Field label={$t('password')} required={!oauthEnabled}>
+        <PasswordInput id="password" bind:value={password} autocomplete="new-password" />
+      </Field>
+
+      <Field label={$t('confirm_password')} required={!oauthEnabled}>
+        <PasswordInput id="confirmPassword" bind:value={passwordConfirm} autocomplete="new-password" />
+        <HelperText color="danger">{passwordMismatchMessage}</HelperText>
+      </Field>
+
+      <Field label={$t('admin.require_password_change_on_login')}>
+        <Switch id="require-password-change" bind:checked={shouldChangePassword} class="flex justify-between text-sm" />
+      </Field>
+
+      <Field label={$t('name')} required>
+        <Input bind:value={name} />
+      </Field>
+
+      <Field label={$t('admin.quota_size_gib')}>
+        <Input bind:value={quotaSize} type="number" min="0" />
+        {#if quotaSizeWarning}
+          <HelperText color="danger">{$t('errors.quota_higher_than_disk_size')}</HelperText>
+        {/if}
+      </Field>
+    </Stack>
+
+    {#snippet stickyBottom()}
+      <Button color="secondary" fullWidth onclick={onCancel} shape="round">{$t('cancel')}</Button>
+      <Button type="submit" disabled={!valid} fullWidth shape="round">{$t('create')}</Button>
+    {/snippet}
+  </FullScreenModal>
+</form>
diff --git a/web/src/lib/components/layouts/AuthPageLayout.svelte b/web/src/lib/components/layouts/AuthPageLayout.svelte
index 3a61a1671c..78ff67cfcb 100644
--- a/web/src/lib/components/layouts/AuthPageLayout.svelte
+++ b/web/src/lib/components/layouts/AuthPageLayout.svelte
@@ -11,7 +11,7 @@
 </script>
 
 <section class="min-w-screen flex min-h-screen items-center justify-center">
-  <Card color="secondary" class="w-full max-w-xl border m-2">
+  <Card color="secondary" class="w-full max-w-lg border m-2">
     <CardHeader class="mt-6">
       <VStack>
         <Logo variant="icon" size="giant" />
diff --git a/web/src/lib/components/shared-components/search-bar/search-filter-modal.svelte b/web/src/lib/components/shared-components/search-bar/search-filter-modal.svelte
index de34092658..c367d001f2 100644
--- a/web/src/lib/components/shared-components/search-bar/search-filter-modal.svelte
+++ b/web/src/lib/components/shared-components/search-bar/search-filter-modal.svelte
@@ -17,7 +17,7 @@
 </script>
 
 <script lang="ts">
-  import Button from '$lib/components/elements/buttons/button.svelte';
+  import { Button } from '@immich/ui';
   import { AssetTypeEnum, type SmartSearchDto, type MetadataSearchDto } from '@immich/sdk';
   import SearchPeopleSection from './search-people-section.svelte';
   import SearchLocationSection from './search-location-section.svelte';
@@ -163,7 +163,7 @@
   </form>
 
   {#snippet stickyBottom()}
-    <Button type="reset" color="gray" fullwidth form={formId}>{$t('clear_all')}</Button>
-    <Button type="submit" fullwidth form={formId}>{$t('search')}</Button>
+    <Button shape="round" size="large" type="reset" color="secondary" fullWidth form={formId}>{$t('clear_all')}</Button>
+    <Button shape="round" size="large" type="submit" fullWidth form={formId}>{$t('search')}</Button>
   {/snippet}
 </FullScreenModal>
diff --git a/web/src/routes/admin/user-management/+page.svelte b/web/src/routes/admin/user-management/+page.svelte
index d93a8a5731..0ce8a1d018 100644
--- a/web/src/routes/admin/user-management/+page.svelte
+++ b/web/src/routes/admin/user-management/+page.svelte
@@ -2,9 +2,6 @@
   import { page } from '$app/stores';
   import DeleteConfirmDialog from '$lib/components/admin-page/delete-confirm-dialogue.svelte';
   import RestoreDialogue from '$lib/components/admin-page/restore-dialogue.svelte';
-  import Button from '$lib/components/elements/buttons/button.svelte';
-  import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
-  import LinkButton from '$lib/components/elements/buttons/link-button.svelte';
   import Icon from '$lib/components/elements/icon.svelte';
   import CreateUserForm from '$lib/components/forms/create-user-form.svelte';
   import EditUserForm from '$lib/components/forms/edit-user-form.svelte';
@@ -15,12 +12,13 @@
     notificationController,
   } from '$lib/components/shared-components/notification/notification';
   import { locale } from '$lib/stores/preferences.store';
-  import { serverConfig, featureFlags } from '$lib/stores/server-config.store';
+  import { featureFlags, serverConfig } from '$lib/stores/server-config.store';
   import { user } from '$lib/stores/user.store';
   import { websocketEvents } from '$lib/stores/websocket';
   import { copyToClipboard } from '$lib/utils';
   import { getByteUnitString } from '$lib/utils/byte-units';
   import { UserStatus, searchUsersAdmin, type UserAdminResponseDto } from '@immich/sdk';
+  import { Button, Code, IconButton, Text } from '@immich/ui';
   import { mdiContentCopy, mdiDeleteRestore, mdiInfinity, mdiPencilOutline, mdiTrashCanOutline } from '@mdi/js';
   import { DateTime } from 'luxon';
   import { onMount } from 'svelte';
@@ -161,22 +159,21 @@
         >
           {#snippet promptSnippet()}
             <div class="flex flex-col gap-4">
-              <p>{$t('admin.user_password_has_been_reset')}</p>
+              <Text>{$t('admin.user_password_has_been_reset')}</Text>
 
-              <div class="flex justify-center gap-2">
-                <code
-                  class="rounded-md bg-gray-200 px-2 py-1 font-bold text-immich-primary dark:text-immich-dark-primary dark:bg-gray-700"
-                >
-                  {newPassword}
-                </code>
-                <LinkButton onclick={() => copyToClipboard(newPassword)} title={$t('copy_password')}>
-                  <div class="flex place-items-center gap-2 text-sm">
-                    <Icon path={mdiContentCopy} size="18" />
-                  </div>
-                </LinkButton>
+              <div class="flex justify-center gap-2 items-center">
+                <Code color="primary">{newPassword}</Code>
+                <IconButton
+                  icon={mdiContentCopy}
+                  shape="round"
+                  color="secondary"
+                  variant="ghost"
+                  onclick={() => copyToClipboard(newPassword)}
+                  title={$t('copy_password')}
+                />
               </div>
 
-              <p>{$t('admin.user_password_reset_description')}</p>
+              <Text>{$t('admin.user_password_reset_description')}</Text>
             </div>
           {/snippet}
         </ConfirmDialog>
@@ -222,31 +219,31 @@
                   class="flex flex-row flex-wrap justify-center gap-x-2 gap-y-1 w-4/12 lg:w-3/12 xl:w-2/12 text-ellipsis break-all text-sm"
                 >
                   {#if !immichUser.deletedAt}
-                    <CircleIconButton
+                    <IconButton
+                      shape="round"
+                      size="large"
                       icon={mdiPencilOutline}
                       title={$t('edit_user')}
-                      color="primary"
-                      size="16"
                       onclick={() => editUserHandler(immichUser)}
                     />
                     {#if immichUser.id !== $user.id}
-                      <CircleIconButton
+                      <IconButton
+                        shape="round"
+                        size="large"
                         icon={mdiTrashCanOutline}
                         title={$t('delete_user')}
-                        color="primary"
-                        size="16"
                         onclick={() => deleteUserHandler(immichUser)}
                       />
                     {/if}
                   {/if}
                   {#if immichUser.deletedAt && immichUser.status === UserStatus.Deleted}
-                    <CircleIconButton
+                    <IconButton
+                      shape="round"
+                      size="large"
                       icon={mdiDeleteRestore}
                       title={$t('admin.user_restore_scheduled_removal', {
                         values: { date: getDeleteDate(immichUser.deletedAt) },
                       })}
-                      color="primary"
-                      size="16"
                       onclick={() => restoreUserHandler(immichUser)}
                     />
                   {/if}
@@ -256,8 +253,7 @@
           {/if}
         </tbody>
       </table>
-
-      <Button size="sm" onclick={() => (shouldShowCreateUserForm = true)}>{$t('create_user')}</Button>
+      <Button shape="round" onclick={() => (shouldShowCreateUserForm = true)}>{$t('create_user')}</Button>
     </section>
   </section>
 </UserPageLayout>
diff --git a/web/src/routes/auth/change-password/+page.svelte b/web/src/routes/auth/change-password/+page.svelte
index 6b91118475..a99e70bacb 100644
--- a/web/src/routes/auth/change-password/+page.svelte
+++ b/web/src/routes/auth/change-password/+page.svelte
@@ -51,9 +51,7 @@
 
       <Field label={$t('confirm_password')} required>
         <PasswordInput bind:value={passwordConfirm} autocomplete="new-password" />
-        {#if errorMessage}
-          <HelperText color="danger">{errorMessage}</HelperText>
-        {/if}
+        <HelperText color="danger">{errorMessage}</HelperText>
       </Field>
 
       <div class="my-5 flex w-full">
diff --git a/web/src/routes/auth/login/+page.svelte b/web/src/routes/auth/login/+page.svelte
index f52face78e..bc062292fc 100644
--- a/web/src/routes/auth/login/+page.svelte
+++ b/web/src/routes/auth/login/+page.svelte
@@ -131,7 +131,7 @@
         <div class="inline-flex w-full items-center justify-center mt-4">
           <hr class="my-4 h-px w-3/4 border-0 bg-gray-200 dark:bg-gray-600" />
           <span
-            class="absolute left-1/2 -translate-x-1/2 bg-white px-3 font-medium text-gray-900 dark:bg-immich-dark-gray dark:text-white"
+            class="absolute left-1/2 -translate-x-1/2 bg-gray-50 px-3 font-medium text-gray-900 dark:bg-neutral-900 dark:text-white"
           >
             {$t('or').toUpperCase()}
           </span>
diff --git a/web/tailwind.config.js b/web/tailwind.config.js
index 12bfd7c604..2d81c28dd0 100644
--- a/web/tailwind.config.js
+++ b/web/tailwind.config.js
@@ -2,7 +2,11 @@ import plugin from 'tailwindcss/plugin';
 
 /** @type {import('tailwindcss').Config} */
 export default {
-  content: ['./src/**/*.{html,js,svelte,ts}', './node_modules/@immich/ui/dist/**/*.{svelte,js}'],
+  content: [
+    './src/**/*.{html,js,svelte,ts}',
+    './node_modules/@immich/ui/dist/**/*.{svelte,js}',
+    '../../ui/src/**/*.{html,js,svelte,ts}',
+  ],
   darkMode: 'class',
   theme: {
     extend: {