How to Build a Mobile Responsive Layout with Panel Drawers in React
The Ontology Playground achieves mobile responsiveness by combining React state-driven conditional rendering with CSS media queries that toggle a fixed-position drawer and bottom tab bar only on viewports narrower than 900px.
The Microsoft Ontology-Playground demonstrates an elegant pattern for adapting complex desktop interfaces to mobile screens without duplicating components. In src/App.tsx and src/styles/app.css, the implementation uses a single piece of React state to control which panel displays in a full-screen drawer, while viewport-specific CSS rules handle the responsive visibility and positioning. This architecture maintains a clean separation between mobile and desktop layout logic.
Managing Panel State with React Hooks
At the core of the mobile layout is a state variable that tracks which side-panel is currently active. In src/App.tsx at lines 57‑58, the component declares:
const [mobilePanel, setMobilePanel] = useState<'graph' | 'quests' | 'inspector' | 'query'>('graph');
When mobilePanel is set to anything other than 'graph', the application displays the drawer overlay. This state-driven approach ensures that the mobile interface remains synchronized with the user's navigation intent.
Conditional Rendering of the Drawer Component
The drawer itself is rendered using a conditional block that checks the current state. Lines 29‑42 in src/App.tsx contain the logic:
{mobilePanel !== 'graph' && (
<div className="mobile-panel-drawer">
<button className="mobile-panel-close"
onClick={() => setMobilePanel('graph')}>✕ Close</button>
{mobilePanel === 'quests' && <QuestPanel />}
{mobilePanel === 'inspector' && (
<>
<SearchFilter />
<InspectorPanel />
</>
)}
{mobilePanel === 'query' && <QueryPlayground />}
</div>
)}
This pattern ensures that QuestPanel, InspectorPanel, or QueryPlayground only render when explicitly requested, keeping the DOM lightweight on desktop viewports where the drawer remains hidden.
Rendering the Bottom Tab Bar
The navigation interface consists of a bottom tab bar defined at lines 13‑27 in src/App.tsx. The tabs update the mobilePanel state when clicked:
<div className="mobile-panel-tabs">
<button className={`mobile-tab ${mobilePanel === 'graph' ? 'active' : ''}`}
onClick={() => setMobilePanel('graph')}>
Graph
</button>
{/* Additional tabs for 'quests', 'inspector', 'query' */}
</div>
The tab bar renders continuously but remains hidden on wide screens via CSS, ensuring seamless transitions between mobile and desktop layouts.
Responsive Visibility via CSS Media Queries
The actual toggling between desktop and mobile views happens in src/styles/app.css. Lines 4189‑4196 define a media query that activates the mobile interface:
.mobile-panel-drawer { display: none; }
@media (max-width: 900px) {
.mobile-panel-drawer {
display: flex;
flex-direction: column;
}
.mobile-panel-tabs { display: flex; flex-shrink: 0; }
}
By default, both .mobile-panel-drawer and .mobile-panel-tabs use display: none. The @media (max-width: 900px) rule overrides this to display: flex, ensuring these elements only appear on narrow viewports.
Fixed Positioning and Layout Structure
Once visible, the drawer requires specific positioning to cover the main content without disrupting the underlying graph view. Lines 4197‑4204 in src/styles/app.css establish:
@media (max-width: 900px) {
.mobile-panel-drawer {
position: fixed;
top: 56px;
bottom: calc(48px + env(safe-area-inset-bottom, 0px));
left: 0; right: 0;
background: var(--bg-secondary);
z-index: 500;
overflow-y: auto;
border-top: 1px solid var(--border-color);
}
}
The fixed positioning spans from top: 56px to bottom: calc(48px + env(safe-area-inset-bottom, 0px)), accounting for both the header and safe area on mobile devices. The high z-index: 500 ensures the drawer floats above interactive canvas elements.
Flex Adjustments for Inner Panels
Inside the drawer, flex properties manage the internal layout. Lines 4226‑4232 configure the panel containers to fill available space while maintaining header integrity:
.mobile-panel-drawer > * {
flex-grow: 1;
display: flex;
flex-direction: column;
min-height: 0;
}
These rules ensure that inner panels expand to fill the drawer while allowing content to scroll appropriately within the constrained viewport.
Closing Drawers and Resetting State
User interaction for closing the drawer is handled by a button that resets the state. At lines 32‑33 in src/App.tsx, the close button calls setMobilePanel('graph'):
<button className="mobile-panel-close"
onClick={() => setMobilePanel('graph')}>✕ Close</button>
When the state returns to 'graph', the conditional rendering removes the drawer from the DOM, revealing the main graph view beneath without requiring a page reload.
Summary
- The React state
mobilePaneltracks which side panel is active, using a union type of'graph' | 'quests' | 'inspector' | 'query'. - Conditional rendering in
src/App.tsxlines 29‑42 displays the drawer only whenmobilePanel !== 'graph'. - A bottom tab bar provides navigation on mobile devices, rendered at lines 13‑27 but hidden via CSS on wide screens.
- CSS media queries at
max-width: 900pxinsrc/styles/app.csslines 4189‑4196 toggledisplay: flexto show mobile elements only on narrow viewports. - Fixed positioning with
z-index: 500ensures the drawer overlays the main content without layout shifts. - The close button resets state to
'graph', removing the drawer and returning focus to the graph visualization.
Frequently Asked Questions
How does the Ontology Playground determine when to show the mobile drawer?
The application checks the mobilePanel React state variable. When this value is anything other than 'graph', the conditional rendering logic in src/App.tsx lines 29‑42 inserts the .mobile-panel-drawer element into the DOM. CSS media queries then ensure this drawer is only visible on screens narrower than 900px.
Why use a max-width of 900px for the mobile breakpoint?
The 900px breakpoint in src/styles/app.css lines 4189‑4196 represents a threshold where the desktop sidebar layout becomes impractical. Below this width, the interface switches to a mobile-first approach with a bottom tab bar and full-screen drawer, optimizing touch interactions and readability on phones and narrow tablets.
How is the drawer positioned to cover the graph without affecting layout?
The drawer uses position: fixed with coordinates spanning top: 56px to bottom: calc(48px + env(safe-area-inset-bottom, 0px)) as defined in src/styles/app.css lines 4197‑4204. This removes the element from the document flow and positions it absolutely relative to the viewport, overlaying the graph canvas with z-index: 500 while maintaining proper spacing for device safe areas.
What happens when a user clicks the close button on the mobile panel?
The close button triggers setMobilePanel('graph') at lines 32‑33 in src/App.tsx, resetting the state to the default graph view. This state change causes React to unmount the drawer component, immediately returning the user to the main graph interface without page reloads.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →