diff --git a/apps/docs/docs/management/users/index.mdx b/apps/docs/docs/management/users/index.mdx
index a7181a1f5..dd14f4a6d 100644
--- a/apps/docs/docs/management/users/index.mdx
+++ b/apps/docs/docs/management/users/index.mdx
@@ -220,7 +220,8 @@ Clicking the `Copy & close` button will copy the URL to your clipboard.
### Self-onboarding
-Clicking the invitation link will bring up a form with fields for the username and password:
+Clicking the invitation link opens a centered registration form that uses the same visual shell as Homarr's onboarding
+and sign-in pages, with fields for the username and password:

diff --git a/apps/nextjs/src/app/[locale]/auth/invite/[id]/page.tsx b/apps/nextjs/src/app/[locale]/auth/invite/[id]/page.tsx
index dbf6306c3..475464aff 100644
--- a/apps/nextjs/src/app/[locale]/auth/invite/[id]/page.tsx
+++ b/apps/nextjs/src/app/[locale]/auth/invite/[id]/page.tsx
@@ -1,13 +1,13 @@
import { notFound } from "next/navigation";
-import { Card, Center, Stack, Text, Title } from "@mantine/core";
+import { IconUserPlus } from "@tabler/icons-react";
import { auth } from "@homarr/auth/next";
import { isProviderEnabled } from "@homarr/auth/server";
import { and, db, eq } from "@homarr/db";
import { invites } from "@homarr/db/schema";
+import { OnboardingAuthShell } from "@homarr/onboarding";
import { getScopedI18n } from "@homarr/translation/server";
-import { HomarrLogoWithTitle } from "~/components/layout/logo/homarr-logo";
import { RegistrationForm } from "./_registration-form";
interface InviteUsagePageProps {
@@ -48,25 +48,14 @@ export default async function InviteUsagePage(props: InviteUsagePageProps) {
const t = await getScopedI18n("user.page.invite");
return (
-
-
-
-
-
- {t("title")}
-
-
- {t("subtitle")}
-
-
-
-
-
-
- {/* eslint-disable-next-line @typescript-eslint/no-non-null-assertion */}
- {t("description", { username: invite.creator.name! })}
-
-
-
+ }
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ footer={t("description", { username: invite.creator.name! })}
+ >
+
+
);
}
diff --git a/apps/nextjs/src/app/[locale]/auth/login/page.tsx b/apps/nextjs/src/app/[locale]/auth/login/page.tsx
index 30eb88ba0..8286c6205 100644
--- a/apps/nextjs/src/app/[locale]/auth/login/page.tsx
+++ b/apps/nextjs/src/app/[locale]/auth/login/page.tsx
@@ -1,15 +1,15 @@
import { redirect } from "next/navigation";
-import { Alert, Card, Center, Code, Stack, Text, Title } from "@mantine/core";
+import { Alert, Code, Text } from "@mantine/core";
import { IconLogin } from "@tabler/icons-react";
import { env } from "@homarr/auth/env";
import { auth } from "@homarr/auth/next";
+import { OnboardingAuthShell } from "@homarr/onboarding";
import { getScopedI18n } from "@homarr/translation/server";
import { sanitizeRedirectionUrl } from "@homarr/validation/redirection-url";
import { env as appEnv } from "~/env";
-import { HomarrLogoWithTitle } from "~/components/layout/logo/homarr-logo";
import { LoginForm } from "./_login-form";
interface LoginProps {
@@ -29,33 +29,20 @@ export default async function Login(props: LoginProps) {
const t = await getScopedI18n("user.page.login");
return (
-
-
-
-
-
- {t("title")}
-
-
- {t("subtitle")}
+ }>
+ {appEnv.DEMO_MODE ? (
+ } color="blue" variant="light">
+
+ Demo mode is enabled. Sign in with username demo and password demo
-
- {appEnv.DEMO_MODE && (
- } color="blue" variant="light" w={64 * 6} maw="90vw">
-
- Demo mode is enabled. Sign in with username demo and password demo
-
-
- )}
-
-
-
-
-
+
+ ) : null}
+
+
);
}
diff --git a/apps/nextjs/src/app/[locale]/layout.tsx b/apps/nextjs/src/app/[locale]/layout.tsx
index 9ecb2f226..cbc64af1f 100644
--- a/apps/nextjs/src/app/[locale]/layout.tsx
+++ b/apps/nextjs/src/app/[locale]/layout.tsx
@@ -11,6 +11,7 @@ import "~/styles/color-scheme.scss";
import "~/styles/scroll-area.scss";
import { notFound } from "next/navigation";
+import { ColorSchemeScript } from "@mantine/core";
import type { DayOfWeek } from "@mantine/dates";
import { NextIntlClientProvider } from "next-intl";
@@ -161,17 +162,16 @@ export default async function Layout(props: {
]);
return (
- // Instead of ColorSchemScript we use data-mantine-color-scheme to prevent flickering
+
{session ? (
<>
diff --git a/packages/onboarding/src/account.tsx b/packages/onboarding/src/account.tsx
index 39002b765..fe31c6eaa 100644
--- a/packages/onboarding/src/account.tsx
+++ b/packages/onboarding/src/account.tsx
@@ -1,7 +1,7 @@
"use client";
import { useEffect, useRef, useState } from "react";
-import { Alert, Button, Group, Paper, Stack, Text, TextInput, ThemeIcon, Title } from "@mantine/core";
+import { Alert, Button, Stack, Text, TextInput } from "@mantine/core";
import { IconArrowRight, IconShieldCheck, IconUserPlus } from "@tabler/icons-react";
import type { z } from "zod/v4";
@@ -17,10 +17,9 @@ import { groupCreateSchema } from "@homarr/validation/group";
import { userInitSchema } from "@homarr/validation/user";
import { didCredentialsSignInFail } from "./account-recovery";
-import { OnboardingBackdrop } from "./onboarding-backdrop";
+import { OnboardingAuthShell } from "./onboarding-auth-shell";
import type { OnboardingStudioProps } from "./types";
import { useOnboardingSounds } from "./use-onboarding-sounds";
-import classes from "./onboarding-studio.module.css";
const AccountShell = ({
title,
@@ -31,29 +30,9 @@ const AccountShell = ({
description: string;
children: React.ReactNode;
}) => (
-
-
-
-
-
-
-
-
-
-
-
-
- {title}
-
- {description}
-
-
- {children}
-
-
-
-
-
+ }>
+ {children}
+
);
export const AccountSetup = ({ environment }: OnboardingStudioProps) => {
diff --git a/packages/onboarding/src/index.ts b/packages/onboarding/src/index.ts
index f3d372527..68389061d 100644
--- a/packages/onboarding/src/index.ts
+++ b/packages/onboarding/src/index.ts
@@ -1,2 +1,3 @@
+export { OnboardingAuthShell } from "./onboarding-auth-shell";
export { OnboardingStudio } from "./onboarding-studio";
export type { OnboardingEnvironment, OnboardingStudioProps } from "./types";
diff --git a/packages/onboarding/src/onboarding-auth-shell.tsx b/packages/onboarding/src/onboarding-auth-shell.tsx
new file mode 100644
index 000000000..70af0c584
--- /dev/null
+++ b/packages/onboarding/src/onboarding-auth-shell.tsx
@@ -0,0 +1,48 @@
+import type { ReactNode } from "react";
+import { Group, Paper, Stack, Text, ThemeIcon, Title } from "@mantine/core";
+
+import { OnboardingBackdrop } from "./onboarding-backdrop";
+import { OnboardingWordmark } from "./onboarding-wordmark";
+import classes from "./onboarding-studio.module.css";
+
+interface OnboardingAuthShellProps {
+ title: ReactNode;
+ description: ReactNode;
+ icon: ReactNode;
+ children: ReactNode;
+ footer?: ReactNode;
+}
+
+export const OnboardingAuthShell = ({ title, description, icon, children, footer }: OnboardingAuthShellProps) => (
+
+
+
+
+
+
+
+
+
+
+
+ {icon}
+
+
+
+ {title}
+
+ {description}
+
+
+ {children}
+
+
+ {footer ? (
+
+ {footer}
+
+ ) : null}
+
+
+
+);
diff --git a/packages/onboarding/src/onboarding-studio.module.css b/packages/onboarding/src/onboarding-studio.module.css
index 3143c73a3..8a9415e02 100644
--- a/packages/onboarding/src/onboarding-studio.module.css
+++ b/packages/onboarding/src/onboarding-studio.module.css
@@ -129,6 +129,31 @@
overflow: clip;
}
+.authShell {
+ display: grid;
+ min-height: 100dvh;
+ padding-block: clamp(1rem, 4vw, 3rem) calc(clamp(1rem, 4vw, 3rem) + env(safe-area-inset-bottom));
+}
+
+.authLayout {
+ width: min(100%, 36rem);
+ margin: auto;
+}
+
+.authBrand {
+ position: relative;
+ z-index: 1;
+}
+
+.authCard {
+ width: 100%;
+}
+
+.authFooter {
+ max-width: 32rem;
+ text-wrap: balance;
+}
+
.studioGrid {
display: grid;
grid-template-columns: 15rem minmax(0, 1fr);