Skip to content

Mobile Portal Chrome — Design Spec

Status: Shipped · June 2026 (nav chrome, then in-place reactivity) Scope: Member portal mobile navigation (top bar, drawer, bottom tab bar, account sheet) + in-place loading/reactivity Published as mobile app: Yes — WebView wrapper, no browser chrome, no OS back button on iOS

Shipped via: nav chrome (top bar / drawer / account sheet / bottom bar) + content-scoped loading & drawer tap feedback (#14080) + reactive bottom bar — search recents & notifications modal (#14083).


Problem Statement

The existing mobile chrome had four gaps:

  1. No back button. Published as a mobile app (WebView), there are no browser navigation controls. Users on deep pages have no way to go back.
  2. Outdated hamburger menu. The ≡ button opened a Bootstrap modal — no animation, no slide-in, it just appeared. Looked and felt like 2016.
  3. Bottom toolbar hidden from anonymous users. Logged-out visitors saw no bottom navigation, which made the portal feel broken on first visit.
  4. Colorful emoji icons. The nav drawer and bottom bar used emoji icons. Multi-color, inconsistent size, unprofessional on a platform that positions itself as a serious community tool.

A fifth gap surfaced after the chrome shipped: navigation gave no loading feedback. Inside the WebView there is no browser progress bar, so a tap on a nav item or an in-content link led to a dead pause while the server responded. See Loading & In-Place Reactivity below.


Native host continuity

The native WebView stays on the Raklet host it launched with so the origin-scoped isNative state is not lost on a custom-domain or subdomain redirect. Server-side redirect suppression requires a valid Raklet.Mobile cookie with IsNative=true; the public ?isNative=true query parameter is not sufficient by itself. An explicit ?isNative=false or malformed query value disables the exemption for that request. Normal browser traffic continues to use permanent redirects to the organisation's canonical custom domain or {permalink}.raklet.com host.

Top Bar (56px, sticky)

Slot Root page (Home/Feed) Deep page (Event, Profile, etc.)
Left ≡ hamburger → opens nav drawer ← back arrow → window.history.back()
Center Org logo (centered) Org logo (centered)
Right Avatar button → opens account bottom sheet Avatar button → opens account bottom sheet
Right (anon) Sign In pill button Sign In pill button

The left slot is context-aware — one consistent position, two behaviors. The hamburger morphs to ✕ when the drawer is open (CSS-only, 250ms spring animation).

Replaces the old Bootstrap modal (#menuModal).

  • Triggered by ≡ on root pages
  • Opens with transform: translateX(-100%) → 0, 300ms spring easing
  • Scrim backdrop fades in simultaneously: rgba(0,0,0,0.45)
  • Close: tap backdrop, tap ✕, or navigate to a page
  • Contains: community navigation only (Members, Events, Membership, Feed, etc.)
  • Nav items are org API-driven — loaded dynamically, not hardcoded
  • Update Navigation link at bottom (admin only)

Account Bottom Sheet (slides up from bottom)

Replaces the profile dropdown that was folded into the mobile drawer.

  • Triggered by avatar tap (top-right)
  • Opens with transform: translateY(100%) → 0, 320ms spring easing
  • Contains: Profile · Payments · Tickets · Settings · Administrator Panel · My Organisations · Log Out

Separating community navigation (drawer) from personal account (bottom sheet) mirrors how Discord, Circle, and Notion handle this split.

Bottom Tab Bar (5 tabs, always visible)

Position Tab Logged in Anonymous
1 Home Active, navigates to feed Same
2 Search Opens search modal (autofocus + recent searches) Same
3 + (center FAB) Floating elevated button, opens create post/event Hidden
4 Notifications Bell + unread badge → opens notifications modal (recent items) Plain link to notifications (sign-in)
5 Profile / Messages Opens account bottom sheet / Messages Replaced by "Sign In"

Change from previous: Bottom bar was hidden for anonymous users. Now it's always visible — it demonstrates product value before asking for commitment (Discord/Mighty Networks pattern).


Loading & In-Place Reactivity (as-built)

Inside the WebView there is no browser progress bar, so navigation has to provide its own feedback, and the chrome should stay put rather than the whole view blanking.

Content-scoped page-loading spinner

On a real same-tab navigation, rakletMobileNav.showPageLoading() drops a branded spinner scoped to the content region — inset below the header (.SocialHeader) and above the fixed bottom bar (.SocialBottomBarMobile .nav) — so the chrome stays visible and the navigation reads as "content swapped", not "the whole app blanked". Falls back to full-bleed if the header isn't found. It is shown synchronously the instant a nav target is tapped and is discarded when the next page paints (pageshow clears it for back/forward-cache restores).

Which taps trigger it: drawer items, account-sheet items, bottom-bar tabs, and in-content links inside .SocialContent (event cards, member profiles, etc.). Gated to the mobile viewport (max-width: 767px); desktop is unchanged. A same-tab-HTTP(S) allowlist (a.protocol) plus exclusions for target=_blank, data-toggle=modal, download, rel~="external", data-ajax/data-remote, and cross-origin links keep the spinner off taps that don't end in a page load.

Drawer item tap — orchestrated feedback

Tapping a drawer/menu item: the row is marked selected immediately (reuses the .list-group-item.active brand tint), the spinner is shown, then the drawer slides closed a beat (~130ms) later — so the wait reads as selected → closing → loading instead of a dead pause.

Search modal — autofocus + recent searches

The Search tab opens #searchModal. The input autofocuses on open so the keyboard appears (best-effort on iOS WebView, which only raises the keyboard for gesture-stack focus; reliable on Android). Recent searches are kept in localStorage, namespaced per org (raklet:recentSearches:<permalink>, cap 8): tap a term to re-run it, × to remove a single entry.

Notifications modal — reuse the desktop AJAX

The Notifications tab opens #mobileNotificationModal instead of navigating to the full page. It lazy-loads recent items from the same endpoint as the desktop header dropdown (GET /App/Notification/GetUserNotifications), renders avatar / actor / description / relative time with an unread highlight, marks unread items read on tap (SetReadNotification, via sendBeacon so it survives the navigation), supports "mark all read" (SetReadAllNotification), and links to the full page via "View all".

Still a full reload

The Messages tab still navigates to the full /App/Messages page — deferred for a later rebuild.


Animation Specification

All animations use transform only — never width, left, right, or height. This keeps them on the GPU compositor thread.

Element Property Duration Easing
Drawer open translateX(-100%) → 0 300ms cubic-bezier(0.16, 1, 0.3, 1)
Drawer close translateX(0) → -100%) 220ms cubic-bezier(0.4, 0, 1, 1)
Sheet open translateY(100%) → 0 320ms cubic-bezier(0.16, 1, 0.3, 1)
Sheet close translateY(0) → 100% 240ms cubic-bezier(0.4, 0, 1, 1)
Backdrop opacity 0 → 0.45 matches drawer/sheet same easing
Hamburger → ✕ line transform 250ms cubic-bezier(0.16, 1, 0.3, 1)
Page-loading overlay opacity 0 → 1 160ms ease

Icon System

Attribute Value
Style SVG stroke/outline
Stroke width 2px
Stroke cap round
Size (bottom bar) 22 × 22px
Size (drawer) 16 × 16px in 32px container
Color inactive #9CA3AF (neutral)
Color active Brand accent (CSS variable, org-overridable)

No emoji. No Font Awesome color icons. One color per icon.


Color & Typography

Colors are CSS variables to support org-level theming:

--portal-brand:   #1B2035;   /* top bar bg, drawer header — org can override */
--portal-accent:  #1A6EF5;   /* active tabs, FAB, active drawer item — org can override */
--portal-surface: #F0F2F5;
--portal-card:    #FFFFFF;
--portal-muted:   #6B7280;
--portal-border:  #E5E7EB;
--portal-badge:   #EF4444;

Typography: DM Sans as platform default (replaces system-ui). Org font setting overrides via --org-font CSS variable. Fallback: -apple-system, sans-serif.


Files Affected

File Change
Application/Views/Shared/_Layout.cshtml Top bar; 5-tab bottom bar; search modal (recent-searches container, data-permalink); notifications modal (#mobileNotificationModal, endpoint data-*)
Application/Views/Shared/_NavigationMobile.cshtml Slide drawer (replaces the Bootstrap modal)
Raklet.Backend/Content/styles/core/social/layout/social-header.scss Hamburger morph animation, back button style
Raklet.Backend/Content/styles/core/social/layout/social-footer.scss 5-tab layout, FAB styles, remove anon display:none
Raklet.Backend/Content/styles/core/social/pages/social-mobile-navigation.scss Slide drawer + backdrop; content-scoped #mobilePageLoading; .RecentSearches*; .MobileNotif*
Raklet.Backend/Content/scripts/core/social/mvc/social.js rakletMobileNav (drawer/sheet/back, page-loading, nav-tap feedback), rakletMobileSearch, rakletMobileNotifications
Resources/Strings.resx (+ .Designer.cs) RecentSearches string

What This Is Not

  • Not a full portal reskin — only the navigation chrome plus the loading/reactivity behaviors above. Page content styles are otherwise unchanged.
  • Not iOS/Android native code — this is mobile web, served via WebView.
  • Not a new font for all pages — DM Sans is the mobile chrome UI font (nav labels, sheet items). Page-level fonts remain under org font settings.
  • Not a full SPA — navigation is still server-rendered page loads; the spinner + chrome-stays-put treatment give the feel of in-place content swaps without an AJAX rewrite. Search and notifications are the genuinely in-place (modal) interactions; Messages is still a full reload.