diff --git a/apps/sim/app/(auth)/components/auth-loading-skeleton.test.tsx b/apps/sim/app/(auth)/components/auth-loading-skeleton.test.tsx
new file mode 100644
index 00000000000..0fff2b496f4
--- /dev/null
+++ b/apps/sim/app/(auth)/components/auth-loading-skeleton.test.tsx
@@ -0,0 +1,54 @@
+/**
+ * @vitest-environment jsdom
+ */
+import type { ReactElement } from 'react'
+import { renderToStaticMarkup } from 'react-dom/server'
+import { describe, expect, it } from 'vitest'
+import {
+ AuthLoadingAlternateActions,
+ AuthLoadingField,
+ AuthLoadingFrame,
+ AuthLoadingSkeleton,
+} from '@/app/(auth)/components/auth-loading-skeleton'
+
+function render(element: ReactElement): HTMLDivElement {
+ const container = document.createElement('div')
+ container.innerHTML = renderToStaticMarkup(element)
+ return container
+}
+
+describe('auth loading skeletons', () => {
+ it.each([
+ ['title', 'h-[38px]', 'rounded-[4px]'],
+ ['label', 'h-[14px]', 'rounded-[4px]'],
+ ['control', 'h-[44px]', 'rounded-[10px]'],
+ ['divider', 'h-[1px]', 'rounded-[1px]'],
+ ] as const)('retains the %s placeholder geometry', (shape, height, radius) => {
+ const container = render()
+ const skeleton = container.firstElementChild
+
+ expect(skeleton?.classList.contains('animate-pulse')).toBe(true)
+ expect(skeleton?.classList.contains('bg-[var(--surface-active)]')).toBe(true)
+ expect(skeleton?.classList.contains(height)).toBe(true)
+ expect(skeleton?.classList.contains(radius)).toBe(true)
+ expect(skeleton?.classList.contains('w-[93px]')).toBe(true)
+ })
+
+ it('keeps one label, three controls, and one divider in the shared field and actions', () => {
+ const container = render(
+
+
+
+
+ )
+ const frame = container.firstElementChild
+ const skeletons = Array.from(container.querySelectorAll('.animate-pulse'))
+
+ expect(frame?.classList.contains('flex-col')).toBe(true)
+ expect(skeletons).toHaveLength(5)
+ expect(skeletons.filter((el) => el.classList.contains('h-[14px]'))).toHaveLength(1)
+ expect(skeletons.filter((el) => el.classList.contains('h-[44px]'))).toHaveLength(3)
+ expect(skeletons.filter((el) => el.classList.contains('h-[1px]'))).toHaveLength(1)
+ expect(skeletons[0].classList.contains('w-[86px]')).toBe(true)
+ })
+})
diff --git a/apps/sim/app/(auth)/components/auth-loading-skeleton.tsx b/apps/sim/app/(auth)/components/auth-loading-skeleton.tsx
new file mode 100644
index 00000000000..be5968cec46
--- /dev/null
+++ b/apps/sim/app/(auth)/components/auth-loading-skeleton.tsx
@@ -0,0 +1,58 @@
+import type { ReactNode } from 'react'
+import { cn, Skeleton } from '@sim/emcn'
+import { cva, type VariantProps } from 'class-variance-authority'
+
+const authLoadingSkeletonVariants = cva('', {
+ variants: {
+ shape: {
+ title: 'h-[38px] rounded-[4px]',
+ label: 'h-[14px] rounded-[4px]',
+ control: 'h-[44px] rounded-[10px]',
+ divider: 'h-[1px] rounded-[1px]',
+ },
+ },
+})
+
+interface AuthLoadingSkeletonProps extends VariantProps {
+ className?: string
+}
+
+/** Placeholder geometry shared by the auth route loading states. */
+export function AuthLoadingSkeleton({ shape, className }: AuthLoadingSkeletonProps) {
+ return
+}
+
+interface AuthLoadingFrameProps {
+ children: ReactNode
+}
+
+export function AuthLoadingFrame({ children }: AuthLoadingFrameProps) {
+ return {children}
+}
+
+interface AuthLoadingFieldProps {
+ labelWidthClassName: string
+ className?: string
+}
+
+export function AuthLoadingField({ labelWidthClassName, className }: AuthLoadingFieldProps) {
+ return (
+
+ )
+}
+
+/** The divider and paired provider-action placeholders on login and signup. */
+export function AuthLoadingAlternateActions() {
+ return (
+ <>
+
+
+ >
+ )
+}
diff --git a/apps/sim/app/(auth)/login/loading.tsx b/apps/sim/app/(auth)/login/loading.tsx
index 74f7bece6c9..5ed074044be 100644
--- a/apps/sim/app/(auth)/login/loading.tsx
+++ b/apps/sim/app/(auth)/login/loading.tsx
@@ -1,24 +1,19 @@
-import { Skeleton } from '@sim/emcn'
+import {
+ AuthLoadingAlternateActions,
+ AuthLoadingField,
+ AuthLoadingFrame,
+ AuthLoadingSkeleton,
+} from '@/app/(auth)/components/auth-loading-skeleton'
export default function LoginLoading() {
return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
)
}
diff --git a/apps/sim/app/(auth)/reset-password/loading.tsx b/apps/sim/app/(auth)/reset-password/loading.tsx
index d1910ac0425..6535a7e6e86 100644
--- a/apps/sim/app/(auth)/reset-password/loading.tsx
+++ b/apps/sim/app/(auth)/reset-password/loading.tsx
@@ -1,16 +1,17 @@
-import { Skeleton } from '@sim/emcn'
+import {
+ AuthLoadingField,
+ AuthLoadingFrame,
+ AuthLoadingSkeleton,
+} from '@/app/(auth)/components/auth-loading-skeleton'
export default function ResetPasswordLoading() {
return (
-
+
+
+
+
+
+
+
)
}
diff --git a/apps/sim/app/(auth)/signup/loading.tsx b/apps/sim/app/(auth)/signup/loading.tsx
index c1190c8385e..bb16f3cd39d 100644
--- a/apps/sim/app/(auth)/signup/loading.tsx
+++ b/apps/sim/app/(auth)/signup/loading.tsx
@@ -1,28 +1,20 @@
-import { Skeleton } from '@sim/emcn'
+import {
+ AuthLoadingAlternateActions,
+ AuthLoadingField,
+ AuthLoadingFrame,
+ AuthLoadingSkeleton,
+} from '@/app/(auth)/components/auth-loading-skeleton'
export default function SignupLoading() {
return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
)
}
diff --git a/apps/sim/app/(auth)/sso/loading.tsx b/apps/sim/app/(auth)/sso/loading.tsx
index 116e47c136b..fc36d8d1835 100644
--- a/apps/sim/app/(auth)/sso/loading.tsx
+++ b/apps/sim/app/(auth)/sso/loading.tsx
@@ -1,16 +1,17 @@
-import { Skeleton } from '@sim/emcn'
+import {
+ AuthLoadingField,
+ AuthLoadingFrame,
+ AuthLoadingSkeleton,
+} from '@/app/(auth)/components/auth-loading-skeleton'
export default function SSOLoading() {
return (
-
+
+
+
+
+
+
+
)
}
diff --git a/apps/sim/app/(auth)/verify/loading.tsx b/apps/sim/app/(auth)/verify/loading.tsx
index d884048905a..4fa87a42400 100644
--- a/apps/sim/app/(auth)/verify/loading.tsx
+++ b/apps/sim/app/(auth)/verify/loading.tsx
@@ -1,12 +1,15 @@
-import { Skeleton } from '@sim/emcn'
+import {
+ AuthLoadingFrame,
+ AuthLoadingSkeleton,
+} from '@/app/(auth)/components/auth-loading-skeleton'
export default function VerifyLoading() {
return (
-
-
-
-
-
-
+
+
+
+
+
+
)
}