/**
* Responsive breakpoint tracking using Svelte 5 runes
*
* Provides reactive viewport dimensions and breakpoint detection that
* automatically updates on window resize. Includes touch device detection
* and orientation tracking.
*
* Default breakpoints match Tailwind CSS:
* - xs: < 640px (mobile)
* - sm: 640px (mobile)
* - md: 768px (tablet portrait)
* - lg: 1024px (tablet)
* - xl: 1280px (desktop)
* - 2xl: 1536px (desktop large)
*
* @example
* ```svelte
*
*
* {#if responsiveManager.isMobile}
*
* {:else}
*
* {/if}
*
*
Viewport: {responsiveManager.width}x{responsiveManager.height}
* Breakpoint: {responsiveManager.currentBreakpoint}
* ```
*/
/**
* Breakpoint definitions for responsive design
*
* Values represent the minimum width (in pixels) for each breakpoint.
* Customize to match your design system's breakpoints.
*/
export interface Breakpoints {
/**
* Mobile devices - default 640px
*/
mobile: number;
/**
* Tablet portrait - default 768px
*/
tabletPortrait: number;
/**
* Tablet landscape - default 1024px
*/
tablet: number;
/**
* Desktop - default 1280px
*/
desktop: number;
/**
* Large desktop - default 1536px
*/
desktopLarge: number;
}
/**
* Default breakpoint values (Tailwind CSS compatible)
*/
const DEFAULT_BREAKPOINTS: Breakpoints = {
mobile: 640,
tabletPortrait: 768,
tablet: 1024,
desktop: 1280,
desktopLarge: 1536,
};
/**
* Device orientation type
*/
export type Orientation = 'portrait' | 'landscape';
/**
* Creates a responsive manager for tracking viewport state
*
* Tracks viewport dimensions, calculates breakpoint states, and detects
* device capabilities (touch, orientation). Uses ResizeObserver for
* accurate tracking and falls back to window resize events.
*
* @param customBreakpoints - Optional custom breakpoint values
* @returns Responsive manager instance with reactive properties
*
* @example
* ```ts
* // Use defaults
* const responsive = createResponsiveManager();
*
* // Custom breakpoints
* const custom = createResponsiveManager({
* mobile: 480,
* desktop: 1024
* });
*
* // In component
* $: isMobile = responsive.isMobile;
* $: cols = responsive.isDesktop ? 3 : 1;
* ```
*/
export function createResponsiveManager(customBreakpoints?: Partial) {
const breakpoints: Breakpoints = {
...DEFAULT_BREAKPOINTS,
...customBreakpoints,
};
// Reactive viewport dimensions
let width = $state(typeof window !== 'undefined' ? window.innerWidth : 0);
let height = $state(typeof window !== 'undefined' ? window.innerHeight : 0);
// Derived breakpoint states
const isMobile = $derived(width < breakpoints.mobile);
const isTabletPortrait = $derived(
width >= breakpoints.mobile && width < breakpoints.tabletPortrait,
);
const isTablet = $derived(
width >= breakpoints.tabletPortrait && width < breakpoints.desktop,
);
const isDesktop = $derived(
width >= breakpoints.desktop && width < breakpoints.desktopLarge,
);
const isDesktopLarge = $derived(width >= breakpoints.desktopLarge);
// Convenience groupings
const isMobileOrTablet = $derived(width < breakpoints.desktop);
const isTabletOrDesktop = $derived(width >= breakpoints.tabletPortrait);
// Orientation detection
const orientation = $derived(height > width ? 'portrait' : 'landscape');
const isPortrait = $derived(orientation === 'portrait');
const isLandscape = $derived(orientation === 'landscape');
// Touch device detection (best effort heuristic)
const isTouchDevice = $derived(
typeof window !== 'undefined'
&& ('ontouchstart' in window || navigator.maxTouchPoints > 0),
);
/**
* Initialize responsive tracking
*
* Sets up ResizeObserver on document.documentElement and falls back
* to window resize event listener. Returns cleanup function.
*
* @returns Cleanup function to remove listeners
*/
function init() {
if (typeof window === 'undefined') {
return;
}
const handleResize = () => {
width = window.innerWidth;
height = window.innerHeight;
};
// Use ResizeObserver for more accurate tracking
const resizeObserver = new ResizeObserver(handleResize);
resizeObserver.observe(document.documentElement);
// Fallback to window resize event
window.addEventListener('resize', handleResize, { passive: true });
// Initial measurement
handleResize();
return () => {
resizeObserver.disconnect();
window.removeEventListener('resize', handleResize);
};
}
/**
* Check if current viewport matches a custom breakpoint range
*
* @param min - Minimum width (inclusive)
* @param max - Optional maximum width (exclusive)
* @returns true if viewport width matches the range
*
* @example
* ```ts
* responsive.matches(768, 1024); // true for tablet only
* responsive.matches(1280); // true for desktop and larger
* ```
*/
function matches(min: number, max?: number): boolean {
if (max !== undefined) {
return width >= min && width < max;
}
return width >= min;
}
/**
* Current breakpoint name based on viewport width
*/
const currentBreakpoint = $derived(
(() => {
switch (true) {
case isMobile:
return 'mobile';
case isTabletPortrait:
return 'tabletPortrait';
case isTablet:
return 'tablet';
case isDesktop:
return 'desktop';
case isDesktopLarge:
return 'desktopLarge';
default:
return 'xs';
}
})(),
);
return {
/**
* Current viewport width in pixels (reactive)
*/
get width() {
return width;
},
/**
* Current viewport height in pixels (reactive)
*/
get height() {
return height;
},
/**
* True if viewport width is below the mobile threshold
*/
get isMobile() {
return isMobile;
},
/**
* True if viewport width is between mobile and tablet portrait thresholds
*/
get isTabletPortrait() {
return isTabletPortrait;
},
/**
* True if viewport width is between tablet portrait and desktop thresholds
*/
get isTablet() {
return isTablet;
},
/**
* True if viewport width is between desktop and large desktop thresholds
*/
get isDesktop() {
return isDesktop;
},
/**
* True if viewport width is at or above the large desktop threshold
*/
get isDesktopLarge() {
return isDesktopLarge;
},
/**
* True if viewport width is below the desktop threshold
*/
get isMobileOrTablet() {
return isMobileOrTablet;
},
/**
* True if viewport width is at or above the tablet portrait threshold
*/
get isTabletOrDesktop() {
return isTabletOrDesktop;
},
/**
* Current screen orientation (portrait | landscape)
*/
get orientation() {
return orientation;
},
/**
* True if screen height is greater than width
*/
get isPortrait() {
return isPortrait;
},
/**
* True if screen width is greater than height
*/
get isLandscape() {
return isLandscape;
},
/**
* True if the device supports touch interaction
*/
get isTouchDevice() {
return isTouchDevice;
},
/**
* Name of the currently active breakpoint (reactive)
*/
get currentBreakpoint() {
return currentBreakpoint;
},
/**
* Initialization function to start event listeners
*/
init,
/**
* Helper to check for custom width ranges
*/
matches,
/**
* Underlying breakpoint pixel values
*/
breakpoints,
};
}
/**
* Singleton responsive manager instance
*
* Auto-initializes on the client side. Use this throughout the app
* rather than creating multiple instances.
*/
export const responsiveManager = createResponsiveManager();
if (typeof window !== 'undefined') {
responsiveManager.init();
}
/**
* Type for the responsive manager instance
*/
export type ResponsiveManager = ReturnType;