Implementing a Responsive Layout Indicator in CSS and JavaScript
A responsive layout indicator helps users understand the current layout state (grid/list, columns, or breakpoints) and can improve discoverability and accessibility. This guide shows a simple, accessible, and responsive implementation using HTML, CSS, and JavaScript that you can adapt to your app or website.
What this does
- Displays a visual indicator of the current layout mode (e.g., Grid vs List) and current breakpoint (mobile/tablet/desktop).
- Updates on window resize and when the layout control is toggled.
- Uses semantic HTML and minimal JavaScript for performance and accessibility.
Files overview
- index.html — UI and indicator markup
- styles.css — responsive styles and indicator visuals
- script.js — logic to detect breakpoints, toggle layout, and update the indicator
index.html
html
<!doctype html> <html lang=“en”> <head> <meta charset=“utf-8” /> <meta name=“viewport” content=“width=device-width,initial-scale=1” /> <title>Responsive Layout Indicator</title> <link rel=“stylesheet” href=“styles.css” /> </head> <body> <header class=“topbar”> <div class=“controls”> <button id=“toggle-layout” aria-pressed=“false” aria-label=“Toggle layout”>Grid</button> </div> <div id=“layout-indicator” class=“layout-indicator” role=“status” aria-live=“polite”> <span class=“mode”>Grid</span> <span class=“sep”>•</span> <span class=“breakpoint”>Mobile</span> </div> </header> <main id=“content” class=“grid”> <article class=“card”>Item 1</article> <article class=“card”>Item 2</article> <article class=“card”>Item 3</article> <article class=“card”>Item 4</article> <article class=“card”>Item 5</article> <article class=“card”>Item 6</article> </main> <script src=“script.js”></script> </body> </html>
styles.css
css
:root{ –gap: 12px; –bg: #0f1724; –card: #111827; –text: #e6edf3; –muted: #9aa6b2; –accent: #60a5fa; } {box-sizing:border-box} html,body{height:100%} body{ margin:0; font-family:system-ui,-apple-system,Segoe UI,Roboto,“Helvetica Neue”,Arial; background:linear-gradient(180deg,#071028 0%,#0f1724 100%); color:var(–text); padding:20px; } .topbar{ display:flex; justify-content:space-between; align-items:center; gap:16px; margin-bottom:18px; } .controls button{ background:transparent; border:1px solid rgba(255,255,255,0.08); color:var(–text); padding:8px 12px; border-radius:8px; cursor:pointer; } .controls button[aria-pressed=“true”]{ background:var(–accent); color:#06203a; border-color:transparent; } .layout-indicator{ display:inline-flex; align-items:center; gap:8px; font-size:14px; color:var(–muted); padding:6px 10px; border-radius:999px; background:rgba(255,255,255,0.03); border:1px solid rgba(255,255,255,0.03); } .layout-indicator .mode{ font-weight:600; color:var(–text); } .layout-indicator .breakpoint{font-weight:500} / Grid and List base / #content{display:grid;gap:var(–gap)} #content.grid{grid-template-columns:repeat(2,1fr)} #content.list{grid-template-columns:1fr} / Card / .card{ background:linear-gradient(180deg,rgba(255,255,255,0.02),transparent); padding:20px; border-radius:10px; border:1px solid rgba(255,255,255,0.04); } / Responsive breakpoints / / Mobile: up to 599px / @media (max-width:599px){ #content.grid{grid-template-columns:repeat(1,1fr)} .layout-indicator{font-size:13px} } / Tablet: 600–959px / @media (min-width:600px) and (max-width:959px){ #content.grid{grid-template-columns:repeat(2,1fr)} } / Desktop: 960px and up */ @media (min-width:960px){ #content.grid{grid-template-columns:repeat(3,1fr)} }
script.js
js
// Constants for named breakpoints that match CSS media queries const BREAKPOINTS = [ {name: ‘Mobile’, mq: window.matchMedia(’(max-width: 599px)’)}, {name: ‘Tablet’, mq: window.matchMedia(’(min-width: 600px) and (max-width: 959px)’)}, {name: ‘Desktop’, mq: window.matchMedia(’(min-width: 960px)’)} ]; const content = document.getElementById(‘content’); const toggleBtn = document.getElementById(‘toggle-layout’); const indicator = document.getElementById(‘layout-indicator’); const modeEl = indicator.querySelector(’.mode’); const bpEl = indicator.querySelector(’.breakpoint’); let mode = ‘Grid’; // default function getActiveBreakpoint(){ for(const bp of BREAKPOINTS){ if(bp.mq.matches) return bp.name; } return ‘Unknown’; } function updateIndicator(){ modeEl.textContent = mode; bpEl.textContent = getActiveBreakpoint(); // update ARIA and button label toggleBtn.setAttribute(‘aria-pressed’, mode === ‘List’); toggleBtn.textContent = mode === ‘Grid’ ? ‘Grid’ : ‘List’; } // Toggle layout mode toggleBtn.addEventListener(‘click’, () => { mode = mode === ‘Grid’ ? ‘List’ : ‘Grid’; content.classList.toggle(‘list’, mode === ‘List’); content.classList.toggle(‘grid’, mode === ‘Grid’); updateIndicator(); }); // Listen to breakpoint changes BREAKPOINTS.forEach(bp => { bp.mq.addEventListener?.(‘change’, updateIndicator); // modern bp.mq.addListener?.(updateIndicator); // fallback }); // Init content.classList.add(‘grid’); updateIndicator(); window.addEventListener(‘resize’, updateIndicator);
Accessibility notes
- The indicator uses role=“status” and aria-live=“polite” so screen readers announce changes.
- The toggle button uses aria-pressed to indicate state.
- Use sufficient color contrast for the indicator text and background.
Tips for enhancement
- Replace CSS media-query detection with CSS container queries if you need container-aware indicators.
- Persist user preference to localStorage and apply on load.
- Animate transitions between Grid/List for smoother UX.
- Add icons (SVG) for visual clarity and hide text visually-only for small screens.
This implementation provides a clear, responsive layout indicator that updates automatically with window size and user toggles. Copy and adapt the code into your project for a lightweight, accessible solution.
Leave a Reply