Skip to content

useResponsiveLayout ​

The one place that decides what "mobile" means. Three bands, and the off-canvas drawer state shared by every layout.

Open in demo

Import ​

ts
import { computedIsMobile, isPreviewFrame, useResponsiveLayout } from '@/layouts/useResponsiveLayout'

Signature ​

ts
function useResponsiveLayout(): {
  isMobile: Readonly<Ref<boolean>>
  isTablet: Readonly<Ref<boolean>>
  isDesktop: Readonly<Ref<boolean>>
  mobileNavOpen: Ref<boolean>
  openMobileNav: () => void
  closeMobileNav: () => void
  railFor: (configuredCollapsed: boolean) => boolean
  toggleSidebar: () => void
}

function isPreviewFrame(): boolean
const computedIsMobile: ComputedRef<boolean>

The three bands ​

BandWidthThe sidebar is
isMobilebelow 768Not in the layout at all — an off-canvas sheet replaces it.
isTablet768 to 1023Present, forced to the icon rail.
isDesktop1024 and upWhatever config.ui.sidebarStyle says.

Returns ​

MemberTypeDescription
isMobile / isTablet / isDesktopReadonly<Ref<boolean>>Which band the window is in.
mobileNavOpenRef<boolean>Drawer state, writable.
openMobileNav / closeMobileNav() => voidOpen and close it.
railFor(configuredCollapsed) => booleanShould the sidebar draw as the icon rail right now?
toggleSidebar() => voidCollapses the sidebar on desktop; toggles the drawer otherwise. Bound to t s.
isPreviewFrame()booleanWhether this is running inside the responsive preview's iframe.
computedIsMobileComputedRef<boolean>isMobile as a computed, for code that needs one.

Example ​

vue
<script setup lang="ts">
import { useResponsiveLayout } from '@/layouts/useResponsiveLayout'
import { useConfig } from '@core/config/useConfig'
import { computed } from 'vue'

const { isMobile, mobileNavOpen, railFor, toggleSidebar } = useResponsiveLayout()
const cfg = useConfig()

const rail = computed(() => railFor(cfg.config.ui.sidebarStyle === 'collapsed'))
</script>

<template>
  <div class="flex">
    <aside v-if="!isMobile" :class="rail ? 'w-16' : 'w-64'">
      <slot name="nav" />
    </aside>

    <button type="button" @click="toggleSidebar()">
      Menu
    </button>

    <div v-if="isMobile && mobileNavOpen" class="fixed inset-0">
      <slot name="nav" />
    </div>
  </div>
</template>

Notes ​

  • The tablet rail is derived, never written. The config is persisted, so writing sidebarStyle: 'collapsed' on a resize would overwrite the reader's stored desktop preference and survive the reload. railFor combines the stored preference with the current band instead.
  • toggleSidebar does two different things on purpose. On desktop, with a layout that has a sidebar, it flips the stored sidebarStyle — the same write the sidebar's own chevron makes. Below lg, or in a layout with no sidebar at any width, there is nothing to collapse, so it opens the drawer.
  • mobileNavOpen is module-scope, so the navbar's hamburger and the drawer itself are the same switch no matter which of the four layouts composed them.
  • isPreviewFrame() reads ?sa-preview from the URL. The responsive preview loads the real app in an iframe, so without this the embedded copy would render its own preview button and nest another frame. Taking it from the URL means no state has to thread through four layouts.