Skip to content

Mobile-Responsive Layout

Loquent uses a three-breakpoint responsive system: 375px (mobile), 768px (tablet), and 1280px+ (desktop). The sidebar becomes a full-screen overlay drawer on mobile, controlled by a hamburger menu with auto-close behavior.

The sidebar_collapsed signal (Signal<bool>) drives the sidebar across all screen sizes.

StateMobile (<768px)Desktop (≥768px)
true (collapsed)Hidden (hidden md:flex)Icon-only (w-20)
false (expanded)Fixed overlay (z-50, w-64) + backdrop (z-40)Full width (w-64)
src/shared/components/sidebar.rs
class: tw_merge!(
"bg-card text-card-foreground flex flex-col transition-all duration-300",
if is_collapsed() {
"hidden md:flex w-20"
} else {
"fixed inset-y-0 left-0 z-50 w-64 md:relative md:inset-auto md:z-auto"
}
),

On mobile, the collapsed state hides the sidebar entirely. When expanded, it renders as a fixed overlay at z-50.

A shared component renders a semi-transparent overlay behind the sidebar on mobile:

src/shared/layouts/app_layout.rs
#[component]
fn MobileSidebarBackdrop(sidebar_collapsed: Signal<bool>) -> Element {
rsx! {
if !sidebar_collapsed() {
div {
class: "fixed inset-0 z-40 bg-black/50 md:hidden",
onclick: move |_| sidebar_collapsed.set(true),
}
}
}
}

Tapping the backdrop closes the sidebar. The md:hidden class ensures it never renders on desktop.

The header renders a toggle button on mobile via an optional sidebar_collapsed prop:

src/shared/components/header.rs
#[props(default)] sidebar_collapsed: Option<Signal<bool>>,
// Renders only on mobile
button {
class: "md:hidden p-2 rounded-lg hover:bg-muted transition-colors",
onclick: move |_| collapsed.set(!collapsed()),
PanelLeft { size: 20 }
}

NavItem accepts Signal<bool> for is_collapsed and closes the sidebar when the user navigates — but only when the sidebar is currently expanded:

src/shared/components/nav_item.rs
onclick: move |_| {
if !is_collapsed() {
is_collapsed.set(true);
}
},

The guard prevents desktop users with an expanded sidebar from losing their preferred layout on every click.

  1. NavItem click — collapses when sidebar is open
  2. Backdrop tapMobileSidebarBackdrop sets sidebar_collapsed(true)
  3. Logout — sidebar collapses before dispatching on_logout

Admin and Settings tabs use a shared icon-only pattern on mobile. Text labels hide below sm (640px), leaving just the icon as the tap target:

src/mods/admin/views/admin_view.rs
TabsList { class: "w-full justify-start overflow-x-auto overflow-y-hidden scrollbar-none",
TabsTrigger { value: "overview",
Shield { size: 16, class: "text-current" }
span { class: "hidden sm:inline", "Overview" }
}
TabsTrigger { value: "organizations",
Building2 { size: 16, class: "text-current" }
span { class: "hidden sm:inline", "Organizations" }
}
// remaining tabs follow same pattern
}

overflow-x-auto overflow-y-hidden scrollbar-none enables horizontal scroll when tabs overflow on narrow screens. The same pattern applies to Settings tabs in settings_form_component.rs.

To add a new icon-only tab, pair your icon component with a span { class: "hidden sm:inline", "Label" } inside TabsTrigger.

The Organizations tab renders two layouts side-by-side — a mobile card view and a desktop grid table. Only one is visible at any breakpoint:

src/mods/admin/components/admin_organizations_tab_component.rs
// Mobile: card view, hidden at md (768px) and above
div { class: "grid grid-cols-1 gap-3 md:hidden",
for item in items {
Link {
to: Route::AdminOrgDetailsView { id: item.id.to_string() },
class: "block rounded-xl border bg-card p-4 space-y-3 transition-colors hover:bg-muted/60",
// Header: icon + org name + activity count
// Badge row: health status, Twilio mode, webhook gaps
// Metrics grid: 4 equal columns
}
}
}
// Desktop: 9-column grid table, hidden below md
div { class: "hidden md:block overflow-hidden rounded-xl border bg-card",
div { class: "grid grid-cols-[minmax(240px,1.4fr)_repeat(7,minmax(90px,1fr))_minmax(170px,1fr)] gap-3 ...",
// Organization | Members | Phones | Calls | Messages | Twilio | SMS events | Webhook gaps | Activity
}
}

Each mobile card contains three sections:

  1. Header row — org icon, name (truncated via truncate), and total activity count
  2. Badge rowflex-wrap badges for health status, Twilio mode, SMS events, webhook gaps, and activity delta
  3. Metrics grid — 4-column grid with Members, Phones, Calls, and Messages counts

Use this dual-render pattern when a desktop table has 6+ columns. Keep the two render paths fully separate — no shared rendering logic.

The AI Costs chart header stacks vertically on mobile and aligns horizontally at sm:

src/mods/admin/components/admin_ai_costs_tab_component.rs
div { class: "flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between",
div { /* Chart title */ }
select {
class: "h-8 w-full sm:w-auto rounded-md border bg-background px-2 text-sm",
// Full-width dropdown on mobile, auto-width on desktop
}
}

Apply w-full sm:w-auto to form controls paired with headings so they fill available space on mobile without overflowing.

Plan list filters stack vertically on mobile and sit inline on desktop:

src/mods/plan/components/plan_list_filter_bar_component.rs
div { class: "flex flex-col md:flex-row md:items-center gap-2 w-full md:w-auto",
div { class: "w-full md:w-44",
SearchableSelect { /* Template filter */ }
}
div { class: "w-full md:w-44",
SearchableSelect { /* Contact filter */ }
}
}

State pills use min-h-[36px] md:min-h-0 to meet mobile touch-target guidelines (36px minimum).

Long plan descriptions truncate on mobile with a toggle:

src/mods/plan/views/plan_details_view.rs
div {
class: if show_full_description() { "" }
else { "max-h-20 overflow-hidden md:max-h-none md:overflow-visible" },
Markdown { content: data.plan.description.clone() }
}
if data.plan.description.len() > 150 {
button {
class: "text-xs text-primary hover:underline mt-1 md:hidden",
onclick: move |_| show_full_description.toggle(),
if show_full_description() { "Show less" } else { "Show more" }
}
}

The plan detail header stacks vertically on mobile. Action buttons use md:ml-auto to right-align only on desktop:

div { class: "flex flex-col gap-2",
h2 { class: "text-lg font-semibold", {data.plan.title.clone()} }
div { class: "flex flex-wrap items-center gap-2",
// status badge, autopilot toggle
div { class: "flex items-center gap-1.5 md:ml-auto",
// Pause / Resume / Stop buttons
}
}
}

The AI instruction toolbar switches from wrapping to horizontal scroll on mobile:

src/shared/ai_builder/components/ai_instruction_toolbar_component.rs
div { class: "flex overflow-x-auto md:flex-wrap items-center gap-1.5 pb-2 md:pb-0",
Button { class: "rounded-full whitespace-nowrap shrink-0", /* ... */ }
}
FileWhat it does
src/shared/layouts/app_layout.rsLayout shells, MobileSidebarBackdrop
src/shared/components/sidebar.rsResponsive sidebar visibility
src/shared/components/header.rsHamburger menu button
src/shared/components/nav_item.rsAuto-close on navigation
src/mods/admin/views/admin_view.rsAdmin icon-only tabs
src/mods/admin/components/admin_organizations_tab_component.rsDual-render Organizations table
src/mods/admin/components/admin_ai_costs_tab_component.rsResponsive chart headers
src/mods/settings/components/settings_form_component.rsSettings icon-only tabs
src/mods/plan/components/plan_list_filter_bar_component.rsFilter stacking
src/mods/plan/views/plan_details_view.rsHeader layout, description truncation
src/shared/ai_builder/components/ai_instruction_toolbar_component.rsHorizontal scroll