Web App Shell and Navigation
Purpose and Scope
The web app shell is the authenticated frame around the main Cal.diy user experience. Its navigation code decides which top-level destinations appear in the persistent sidebar on larger screens, which destinations remain in the bottom mobile bar, and which items are moved behind a mobile “more” affordance. This page focuses on that shell-level navigation source rather than every page behind each route. It is intended for contributors who need to add, rename, hide, or reason about primary navigation entries without breaking desktop, mobile, embedded, or installed-app contexts.
Sources: apps/web/modules/shell/navigation/Navigation.tsx
The navigation source uses a compact data model: each item describes its translation key, destination, icon, optional badge, mobile behavior, and route-matching predicate. That data is then split into desktop and mobile collections before rendering. This matters because the same conceptual navigation tree is consumed by different shell components rather than being duplicated in separate sidebar and mobile-bar implementations. When changing a top-level item, start with the item definition and then check how the filtering rules classify it for desktop, bottom mobile navigation, and the mobile more menu.
Sources: apps/web/modules/shell/navigation/Navigation.tsx
Relevant Source Files
- apps/web/modules/shell/navigation/Navigation.tsx — Defines the top-level navigation item list, route-current logic, desktop rendering, mobile rendering, authentication and standalone checks, and embed hiding behavior.
- apps/web/package.json — Identifies the web application package, its development and production scripts, and the workspace dependencies that support the shell, embeds, UI primitives, and app-store surfaces.
- apps/web/modules/apps/components/index.ts — Re-exports app-store UI components used by the apps area that the shell navigation links into, including app lists, cards, sliders, loaders, and category navigation.
System-to-Code Mapping
The main entry point is the Navigation component. It obtains desktopNavigationItems from the navigation hook and renders each item with NavigationItem. The source list includes event types, bookings, availability, apps, and a special more separator. The bookings item is notable because it includes an UnconfirmedBookingBadge and a custom current-route predicate for all booking routes. The apps item is also special: it is marked for the mobile more area, has child destinations for the app store and installed apps, and includes route logic that accounts for both normal and versioned app paths.
Sources: apps/web/modules/shell/navigation/Navigation.tsx
The shell separates definition from presentation with useNavigationItems. The hook calls the item factory, removes the more separator from desktop navigation, keeps items that are not mobile-more-only in the mobile bottom bar, and collects moreOnMobile entries for the additional mobile menu. This is a small but important constraint for maintainers: a navigation item is not just added to a visual list; it is classified by flags. The MORE_SEPARATOR_NAME constant is part of that classification, so it should be treated as a structural marker rather than a normal route users are expected to visit.
Sources: apps/web/modules/shell/navigation/Navigation.tsx
| Concern | Source-level implementation | Contributor implication |
|---|---|---|
| Desktop primary navigation | Navigation maps desktopNavigationItems through NavigationItem | Add normal top-level destinations here when they should be visible in the sidebar |
| Mobile bottom navigation | MobileNavigation maps mobileNavigationBottomItems through MobileNavigationItem | Avoid overcrowding the bottom bar by using moreOnMobile for lower-priority sections |
| Mobile overflow | useNavigationItems derives mobileNavigationMoreItems from moreOnMobile entries | Items moved to mobile more must still have clear labels and current-state behavior |
| Bookings signal | The bookings item renders UnconfirmedBookingBadge | Badge-bearing items should be checked for visual fit in both desktop and mobile shells |
| Apps current state | Apps predicates handle /apps, /apps/installed, and /v2/apps variants | Route matching must account for server/client path differences already documented in comments |
Execution Flow
At runtime, the shell starts by calculating a stable navigation model with React memoization. The desktop shell renders immediately from the desktop subset, and it also includes a KBarTrigger element for command-bar access on narrower large-layout breakpoints. Mobile rendering is gated more aggressively. MobileNavigationContainer reads the NextAuth session status and the standalone-app state before deciding whether to mount the mobile navigation at all. If the user is not authenticated, or if the application is running in standalone mode, the container returns no navigation rather than showing a partially useful shell.
Sources: apps/web/modules/shell/navigation/Navigation.tsx
The final mobile component performs one more context check: it calls the embed-core iframe hook to detect whether the web app is running as an embed. When embedded, the bottom navigation is hidden through its class composition rather than being treated as a normal authenticated mobile shell. This distinction prevents an embedded booking or application surface from inheriting the full Cal.diy mobile chrome. The same component also adds padding below the content on small screens, which keeps page content from being covered by the fixed bottom navigation when the bar is visible.
Sources: apps/web/modules/shell/navigation/Navigation.tsx, apps/web/package.json
Apps Navigation and Application Surfaces
The apps navigation item points users to the app-store area and carries child entries for the general app store and installed apps. The child current-state functions intentionally distinguish browsing apps from managing installed apps. The installed-apps child treats both installed app routes and versioned installed app routes as current, while the app-store child excludes installed routes. This gives the shell enough information to highlight the correct branch as users move between catalog browsing and integration management, even when the rendered route path differs between server rendering and client navigation.
Sources: apps/web/modules/shell/navigation/Navigation.tsx
The apps area itself exposes a small public component barrel from the modules directory. It re-exports AllApps, AppCard, Slider, SkeletonLoader, PopularAppsSlider, RecentAppsSlider, and AppStoreCategories. For navigation work, that means the shell route is only the doorway into a separate app-store UI surface. Contributors changing the apps menu should coordinate route labels and highlighting with the components that render the destination experience, especially category browsing, popular or recent app sliders, and loading states. The navigation file should not become the place where app catalog presentation details are implemented.
Sources: apps/web/modules/apps/components/index.ts, apps/web/modules/shell/navigation/Navigation.tsx
Web Package Context
The web application package is published internally as a private workspace named @calcom/web. Its manifest shows the scripts a contributor normally uses while testing navigation changes: development runs through Next.js with Turbopack after copying app-store static assets, production builds run Next build followed by Sentry release creation, and start delegates to Next start. There are also dedicated scripts for type checking, linting, HTTPS development, cron testing, locale checks, Playwright code generation, and Stripe webhook forwarding. Navigation contributors should use those scripts rather than inventing package-local commands.
Sources: apps/web/package.json
The dependency list in the web package also explains why the navigation component imports shared packages instead of implementing everything locally. The shell pulls embed detection from the embed-core workspace, standalone detection from the lib workspace, class composition from the UI workspace, and session status from the authentication stack. The same package depends on app-store, embed-react, embed-snippet, features, prisma, trpc, platform packages, and shared UI libraries. This reinforces a monorepo pattern: the web shell assembles product behavior from package surfaces, while specialized packages own embed runtime, reusable UI primitives, and integration-specific application code.
Sources: apps/web/package.json, apps/web/modules/shell/navigation/Navigation.tsx
Implementation Details and Edge Cases
The most important edge case in the current navigation logic is the apps path mismatch noted directly in the route predicates. During server rendering, the path can appear under a versioned apps route, while on the client it becomes the unversioned apps route. The current checks therefore use starts-with tests rather than strict equality for the parent apps route, and installed apps explicitly accepts both versioned and unversioned installed paths. When adding new children below apps, use similarly tolerant matching if the route can differ between render phases or route aliases.
Sources: apps/web/modules/shell/navigation/Navigation.tsx
A second practical edge case is context-specific suppression. The mobile container deliberately avoids showing navigation for unauthenticated users and for standalone mode, while the mobile nav itself hides in embed mode. These checks protect specialized experiences from inheriting a full application shell. For example, an embed should not expose the same bottom app navigation as an authenticated dashboard, and a standalone context may already provide its own navigation affordances. Before adding a new navigation entry, test the authenticated desktop shell, authenticated mobile shell, embedded view, and standalone behavior when those contexts are relevant.
Sources: apps/web/modules/shell/navigation/Navigation.tsx
Change Checklist
When updating shell navigation, first decide whether the destination is a primary user task or a secondary task. Primary destinations can remain in the desktop list and mobile bottom list; secondary destinations should use the mobile more classification. Next, add or update the item definition with a stable translation key, href, icon, and current-state predicate. Then verify badge behavior if the item renders dynamic status. Finally, run the web package development or type-check scripts and manually inspect desktop, mobile, apps subroutes, embedded contexts, and authenticated session behavior before opening a change.
Sources: apps/web/modules/shell/navigation/Navigation.tsx, apps/web/package.json
Related Pages
- Apps Overview — Use this when changing where the apps navigation item sends users or how app-store concepts are presented.
- Embeds Overview — Use this when validating why embedded contexts suppress the mobile navigation shell.
- Monorepo Architecture — Use this for broader workspace script and package-boundary context.
- UI and COSS UI Components — Use this when changing shared navigation item rendering, icons, class composition, or visual primitives.