diff --git a/apps/web/src/components/layout/mobile-tab-indicator.tsx b/apps/web/src/components/layout/mobile-tab-indicator.tsx
new file mode 100644
index 000000000..30c00dc53
--- /dev/null
+++ b/apps/web/src/components/layout/mobile-tab-indicator.tsx
@@ -0,0 +1,18 @@
+import { m } from "motion/react";
+import { TAB_SPRING } from "@/libs/motion";
+
+/**
+ * The phone tab bar's marker: a short accent bar on the active tab's top edge. Render it inside the active tab
+ * (which must be `relative`); when another tab becomes active it springs across to it. Reduced motion moves it at
+ * once (see `followReducedMotion`).
+ */
+export function MobileTabIndicator() {
+ return (
+
+ );
+}
diff --git a/apps/web/src/libs/motion.ts b/apps/web/src/libs/motion.ts
index 661cd23d5..9abdede02 100644
--- a/apps/web/src/libs/motion.ts
+++ b/apps/web/src/libs/motion.ts
@@ -20,6 +20,9 @@ export const EXIT = 0.7;
/** --ease-out-strong. Landing page only (features/homepage/site-footer.tsx); app code uses EASE. */
export const EASE_OUT_STRONG = [0.23, 1, 0.32, 1] as const;
+/** Springy, a little elastic: the mobile tab bar's marker hopping to the new tab (interruptible mid-flight). */
+export const TAB_SPRING = { type: "spring", duration: 0.4, bounce: 0.3 } as const;
+
/** dnd-kit timing for things settling into place (sortable shuffles, drop animations): on-screen movement. */
export const DRAG_SETTLE = { duration: D2 * 1000, easing: `cubic-bezier(${EASE_MOVE.join(", ")})` };
diff --git a/apps/web/src/routes/builder/$resumeId/-components/editor-shell.tsx b/apps/web/src/routes/builder/$resumeId/-components/editor-shell.tsx
index 0d94900a9..7766eed12 100644
--- a/apps/web/src/routes/builder/$resumeId/-components/editor-shell.tsx
+++ b/apps/web/src/routes/builder/$resumeId/-components/editor-shell.tsx
@@ -8,6 +8,7 @@ import { Icon } from "@reactive-resume/ui/components/icon";
import { Tabs, TabsContent } from "@reactive-resume/ui/components/tabs";
import { useBreakpoint } from "@reactive-resume/ui/hooks/use-breakpoint";
import { cn } from "@reactive-resume/utils/style";
+import { MobileTabIndicator } from "@/components/layout/mobile-tab-indicator";
import {
AssistantColumn,
AssistantOverlay,
@@ -285,18 +286,12 @@ function MobileBody({ mode, onModeChange }: MobileBodyProps) {
if (tab !== "page") onModeChange(tab);
}}
className={cn(
- "flex min-h-[52px] flex-1 flex-col items-center justify-center gap-0.5 text-[11px]",
+ "relative flex min-h-[52px] flex-1 flex-col items-center justify-center gap-0.5 text-[11px] transition-[color,scale] duration-quick ease-enter active:scale-[0.97]",
active ? "font-semibold text-ink" : "text-ink-2",
)}
>
-
-
-
+ {active && }
+
{labels[tab]}
);
diff --git a/apps/web/src/routes/builder/letter/-components/letter-shell.tsx b/apps/web/src/routes/builder/letter/-components/letter-shell.tsx
index e8f388e84..e1a07b482 100644
--- a/apps/web/src/routes/builder/letter/-components/letter-shell.tsx
+++ b/apps/web/src/routes/builder/letter/-components/letter-shell.tsx
@@ -10,6 +10,7 @@ import { Tabs, TabsContent } from "@reactive-resume/ui/components/tabs";
import { toast } from "@reactive-resume/ui/components/toast";
import { useBreakpoint } from "@reactive-resume/ui/hooks/use-breakpoint";
import { cn } from "@reactive-resume/utils/style";
+import { MobileTabIndicator } from "@/components/layout/mobile-tab-indicator";
import {
AssistantColumn,
AssistantOverlay,
@@ -213,18 +214,12 @@ function MobileBody({ mode, onModeChange }: MobileBodyProps) {
if (tab !== "page") onModeChange(tab);
}}
className={cn(
- "flex min-h-[52px] flex-1 flex-col items-center justify-center gap-0.5 text-[11px]",
+ "relative flex min-h-[52px] flex-1 flex-col items-center justify-center gap-0.5 text-[11px] transition-[color,scale] duration-quick ease-enter active:scale-[0.97]",
active ? "font-semibold text-ink" : "text-ink-2",
)}
>
-
-
-
+ {active && }
+
{labels[tab]}
);
diff --git a/apps/web/src/routes/dashboard/-components/app-shell.tsx b/apps/web/src/routes/dashboard/-components/app-shell.tsx
index b2d0f92c0..f08bc7921 100644
--- a/apps/web/src/routes/dashboard/-components/app-shell.tsx
+++ b/apps/web/src/routes/dashboard/-components/app-shell.tsx
@@ -13,6 +13,7 @@ import { Tooltip, TooltipContent, TooltipTrigger } from "@reactive-resume/ui/com
import { useBreakpoint } from "@reactive-resume/ui/hooks/use-breakpoint";
import { getInitials } from "@reactive-resume/utils/string";
import { cn } from "@reactive-resume/utils/style";
+import { MobileTabIndicator } from "@/components/layout/mobile-tab-indicator";
import { useDialogStore } from "@/dialogs/store";
import { applicationsListQueryOptions } from "@/features/applications/queries";
import { useCommandPaletteStore } from "@/features/command-palette/store";
@@ -289,6 +290,7 @@ function MobileTabs() {
const tab = (icon: IconName, label: string, current: boolean) => (
<>
+ {current && }
{label}
>
@@ -302,14 +304,16 @@ function MobileTabs() {
{tab("description", t`Documents`, isCurrent("/dashboard"))}
{tab("work", t`Applications`, isCurrent("/dashboard/applications"))}
@@ -329,7 +333,8 @@ function MobileTabs() {
{tab("account_circle", t`Account`, isCurrent("/dashboard/settings"))}