MobTranslate styleguide

Scaffolds and canonical layouts

Assemble adaptive regions around a real task, preserve state through structural changes and make every pane reachable.

Window, grid and scaffold have different jobs

Begin with tasks and relationships, then assign space. A grid of equal rectangles cannot by itself decide which content is primary. Browser CSS pixels, physical pixels and native density-independent units are distinct; the lab measures CSS pixels only.

ConceptWeb responsibilityVerification
Window or viewportThe space made available by the browser, including resizing and zoomUse actual available space; do not infer a device from a user agent
GridAlignment relationships and tracks inside an available regionCheck content minimums, margins and gaps
ScaffoldThe product arrangement of bars, rails and content panesIdentify each region’s task and reading order
PaneA coherent task, record or supporting contextKeep identity, state and recovery attached to that pane
RulerA shared alignment edge for related titles/contentA design alignment aid, not automatically a draggable widget
Drag handleAn optional affordance for changing a boundaryExpose operation, limits and a keyboard/single-pointer alternative

Bars, safety regions and rails

Bars organize navigation or actions along a page edge. Rails reserve inline-edge space for persistent navigation or tools. They are scaffold regions, not interchangeable ARIA roles: a destination rail needs navigation semantics; a tool rail may contain a labelled toolbar. Keep the main task identifiable and avoid multiple indistinguishable navigation landmarks.

Reserve space for any fixed chrome and device/browser occlusion before placing task controls. Safe-area environment values can help with cutouts, but do not solve zoom, software keyboards or browser controls automatically. Flow placement is often the simplest way to prevent content from being covered.

PlacementContractFailure
Top barPage/task identity and relevant actions; readable titleA sticky bar covers the focused heading
Bottom barReachable primary destinations with reserved content spaceLast result or action is hidden behind the bar
Inline-start railStable navigation and logical boundary/paddingCSS reversal disagrees with tab order
Supporting toolsNamed commands associated with the taskA tool rail is mislabelled as primary navigation
Safety regionDeliberate reserved inset around inaccessible edgesPadding alone is claimed to prove every hardware condition
css
.fixed-bottom-navigation {
  padding-block-end: env(safe-area-inset-bottom, 0px);
}
.task-with-fixed-navigation {
  /* Use the actual bar size, including wrapped labels. */
  padding-block-end: var(--measured-navigation-size);
}
.task-heading { scroll-margin-block-start: var(--measured-header-size); }
/* Measure chrome and verify keyboard/zoom; do not assume a fixed 56px bar. */

Choose a canonical arrangement

A feed card should not become a list-detail selection merely because it is drawn beside another card. An inspector should not replace the task it supports. For MobTranslate, source and review qualifications remain connected to the exact entry even when supporting material moves.

ArrangementUse whenCompact adaptationExpanded adaptation
List-detailA collection and the selected record belong to one taskShow the collection or its selected detail with an explicit return pathKeep selection visible beside the record
FeedPeople scan independent items rather than a master/detail hierarchyUse one readable item columnAdd columns only when individual items remain usable
Supporting paneContext supports an already identifiable primary activityKeep necessary context after the activity or reveal it deliberatelyPlace subordinate context beside the task
Three panesA third region has a distinct useful jobPrioritize which task/context remains visible and provide return pathsGive each region a minimum and bounded relationship
Custom arrangementThe task genuinely differs from these relationshipsDocument reading order, navigation and restorationDo not turn extra width into unrelated panels

Reference classes and local web thresholds

The captured May 2026 overview renames window size classes to breakpoints and responsive layout to adaptive design. These published width categories are context for design handoff. They are not aliases for this library’s sm/md/lg CSS tokens and are not inferred from device names. The Layout chapter records the actual committed web token breakpoints. The lab uses its own measured container thresholds because a pane can be narrow inside a wide browser window.

Height and input conditions remain independent. Test an unusually short wide window, enlarged text, touch and keyboard rather than assuming a width class proves ergonomics.

Material classReference window width in dpLocal adaptation question
CompactBelow 600Can one task remain readable with reachable navigation?
Medium600 to below 840Does a second low-density pane fit without squeezing controls?
Expanded840 to below 1200Does an adjacent record/context help the task?
Large1200 to below 1600Should margins or supporting context use the added room?
Extra-large1600 and aboveIs a third region useful, or merely more distraction?

Working composition and retained-state lab

Open the public English example records, edit distinct notes and save local snapshots. Switch composition, resize and revisit the records: note identity belongs to the record, not its grid position. A saved snapshot stays fixed while its draft changes.

List-detail uses the actual measured container at 640 CSS px, not a forced canvas wider than the page. Supporting context moves beside the task at 900px. Feed tracks change at these same demonstration thresholds. These are local, content-tested web decisions, not copied Material window-class thresholds.

The leading-pane range supplies a keyboard and single-pointer alternative to dragging. Each applicable composition retains its chosen share while stacked. Changing a share does not persist user preferences or resize a native device window.

Adjust the split with the native range control. The share applies only when both panes fit; it is retained while stacked. No drag handle is required.

Measuring the available container. The maximum is a cap, not a forced width.

Local example records

  • Example A

  • Example B

  • Example C

All notes and snapshots belong to this page instance. They survive composition, size and record changes, but not reload. English fixture text keeps its language and glyph order when placement is RTL. This is not a foldable-device, browser-history or remote-storage implementation.

Pane identity, navigation and transitions

  • Keep record IDs and task state independent of which panes are mounted or visible. Layout changes must not submit, clear or duplicate an edit.
  • Compact detail needs a visible return action and a defined focus destination. Restore the originating record trigger, or a meaningful surviving control when it no longer exists.
  • Expanded collection/detail can show two panes without creating two independent primary routes. Decide whether selection is addressable; use actual URL/history state when the product requires deep links.
  • Feed detail can appear after the feed or in a separate destination. Preserve the selected identity and the return point; opening detail is not evidence that an item was read.
  • Supporting context follows its task in DOM order. If essential qualifications are hidden behind disclosure, keep a visible truthful summary beside the result.
  • In the lab, opening moves focus to the detail heading; returning restores the record button. Resizing while a selected list trigger has focus transfers focus before that list becomes hidden. Browser history is intentionally outside this local lab; the Navigation pattern supplies its separate URL/history implementation.

Persistent and temporary resizing

Persistent resizing changes the normal split or preferred size of a region. Temporary expansion gives a region more space for a particular task and needs a clear restore path. Define minimums from actual controls, text and targets; preserve the user’s preference when a compact layout cannot currently apply it.

A handle is optional. If used, distinguish a separator from a draggable divider, expose its direction/value/limits and provide documented keys or a named range/size control. Pointer cancellation must not accidentally publish or lose state. Do not repurpose a decorative divider into an unnamed drag-only input.

The lab implements bounded preferred splits with native range inputs, in memory. It does not implement an overlay expansion or claim cross-session persistence. For a production preference, validate the stored version and scope before applying it to a later workspace.

Resize stateRequired behavior
Preferred splitStore separately from draft/record state
Minimum or maximumBound the value; keep all essential controls reachable
Compact layoutStack or switch panes; retain the dormant preference
Temporary expansionName the expanded region and provide restore/close
Interrupt/cancelKeep a recoverable prior size and unsubmitted work
Focus during resizeMove focus only if its region is about to become unavailable

Reveal, divide, resize, reposition and swap

Available height matters as well as width. A short wide window may need scrolling and shorter nonessential chrome; it does not justify reducing text or target size. Changes in pointer, keyboard, posture or motion preference may affect behavior independently of width. Check mobile, desktop and spatial handoffs against the actual platform contract.

Adaptive decisionExampleInvariant
RevealShow supporting context when enough space remainsRequired evidence is reachable at every size
DivideUse list and detail tracks for one selected recordSelection identity is unchanged
ResizeBound pane shares and wrap long textControls retain names, targets and complete values
RepositionMove context below its task using normal DOM/grid flowReading and keyboard order remain coherent
SwapUse a temporary drawer instead of a persistent navigation railDestinations and current-route truth remain identical
LevitateTemporarily present content over another regionName context, contain focus when modal, restore on close
Spatial panelsAdapt related tasks to a spatial environmentDo not equate CSS pane pixels with physical/headset units

Grids, rulers, grouping and spacing

Align title and content edges deliberately. Start with outer margins and the minimum readable task, then place bars/rails and divide the remaining space. Use separate alignment rules when a full-width bar and a bounded reading column have different jobs; do not force every element into equal tracks.

Rhythm and repeated alignment make relationships recognizable. Proximity groups a label with its field and source context with its entry. Larger gaps can distinguish tasks; similar spacing alone should not imply that unrelated actions have the same importance. Keep negative space around focal actions without pushing required context beyond discovery.

  • Use grid for aligned tracks and flex wrapping for a sequence of actions. Set min-inline-size: 0 on flexible pane content.
  • Derive spacing from committed tokens; use the existing Spacing chapter to inspect the actual scale.
  • Keep prose measure bounded while data regions use deliberate named local scrolling.
  • Treat ruler choices as alignment decisions: title edges, content edges, bar/safety edges and any intentional exceptions.
  • Test a long field label, a multi-line action and mixed-length records before accepting the alignment.

Information density and unit boundaries

Density changes how much information is visible, not whether people can reach and read it. Adjust hierarchy, spacing and progressive disclosure before shrinking every component. A dense table can coexist with a comfortable task action and readable help text.

Display pixel density is a hardware/rendering concern; an information-density preference is a product concern. CSS px are neither physical pixels nor an automatic conversion to Android dp. Do not apply a universal scaling multiplier to fonts, hit targets and icons and then claim accessibility.

  • Keep the chosen target size independently of the visual glyph size.
  • Preserve line height, wrapping and complete error/source qualifications at compact density.
  • If density is a setting, keep it separate from text enlargement and the browser’s zoom.
  • Verify minimum pane sizes with real content and every relevant control state, not a blank wireframe.

Bidirectionality and meaningful mirroring

Use logical edges for margins, rail borders and pane placement. A list-detail collection at inline start naturally moves to the opposite physical edge in RTL, while record and focus order remain explicit. Feed sequence should follow the intended reading direction without reversing DOM order.

Mirror a directional action only when its meaning follows reading direction. Media playback, clocks, written glyphs and many symbols do not become correct by flipping their geometry. Mixed text, dates and identifiers need deliberate direction isolation and language metadata. The Internationalization and Iconography chapters supply those contracts.

The lab changes region placement only; its text is original English fixture text. This does not verify an actual translated RTL interface, touch swipe convention or glyph shaping.

Layering, folds and failure recovery

  • Keep overlays in a documented stacking system. Modal focus containment, Escape and return behavior are different from an ordinary adjacent pane.
  • Treat a fold, hinge, keyboard or unsafe region as a real platform boundary. Avoid putting a continuous control or critical text across an inaccessible gap.
  • Do not infer a hinge from screen width alone. Without a validated platform signal, retain a usable regular-window fallback.
  • Retain edited work and selected identity through window changes. A new arrangement must not claim a remote save.
  • Check empty collections, unavailable records, long content and failed data loading without removing every recovery control.
  • Use the shared Dialog/Drawer patterns for modal content, and the XR chapter for spatial units/capabilities. This container example is not hardware qualification.

Compiled implementation

This source is the rendered container/state model. CSS container queries own track geometry; ResizeObserver owns actual-width reporting and compact list/detail availability. Values are local page state and do not authorize language publication.

tsx
'use client';

import {useEffect,useRef,useState,type CSSProperties} from 'react';
import {Button} from '@mobtranslate/ui';
import './layout-compositions.css';

type Composition='list-detail'|'feed'|'supporting';
const records=[
  {id:'a',title:'Example A',description:'A short English interface record for comparing reading and editing regions.',context:'Illustrative record A. No language entry or publication permission is asserted.'},
  {id:'b',title:'Example B',description:'A second local record with its own independently retained note.',context:'Illustrative record B. Editing a note does not change a source or review status.'},
  {id:'c',title:'Example C',description:'A longer local example for testing wrapping, selection and a stable return path.',context:'Illustrative record C. These records are public documentation fixtures.'},
] as const;
type RecordId=typeof records[number]['id'];

export function LayoutCompositionsExample(){
  const [mode,setMode]=useState<Composition>('list-detail');
  const [requestedWidth,setRequestedWidth]=useState(960);
  const [actualWidth,setActualWidth]=useState<number|null>(null);
  const [rtl,setRtl]=useState(false);
  const [shares,setShares]=useState({'list-detail':34,supporting:66});
  const [selected,setSelected]=useState<RecordId|null>(null);
  const [drafts,setDrafts]=useState<Partial<Record<RecordId,string>>>({});
  const [saved,setSaved]=useState<Partial<Record<RecordId,string>>>({});
  const [status,setStatus]=useState('');
  const [openEpoch,setOpenEpoch]=useState(0);
  const [returnId,setReturnId]=useState<RecordId|null>(null);
  const root=useRef<HTMLDivElement>(null),detail=useRef<HTMLHeadingElement>(null);
  const triggers=useRef<Partial<Record<RecordId,HTMLElement>>>({});
  const compact=actualWidth===null||actualWidth<640;
  const share=mode==='feed'?50:shares[mode];
  const active=records.find(record=>record.id===selected)??(mode==='supporting'?records[0]:null);
  const showList=mode!=='supporting'&&(mode==='feed'||!compact||selected===null);
  const showDetail=mode==='supporting'||(mode==='list-detail'&&!compact)||selected!==null;

  useEffect(()=>{
    const element=root.current;if(!element)return;
    const observer=new ResizeObserver(entries=>{
      const width=entries[0]?.contentRect.width;if(width===undefined)return;
      // If resizing will hide a focused list item, keep the current task reachable.
      if(mode==='list-detail'&&width<640&&selected&&element.querySelector('[data-composition-list]')?.contains(document.activeElement))detail.current?.focus();
      setActualWidth(width);
    });observer.observe(element);return()=>observer.disconnect();
  },[mode,selected]);
  useEffect(()=>{if(openEpoch)detail.current?.focus();},[openEpoch]);
  useEffect(()=>{if(returnId){triggers.current[returnId]?.focus();setReturnId(null);}},[returnId]);
  const choose=(id:RecordId)=>{setSelected(id);setStatus('');setOpenEpoch(value=>value+1);};
  const back=()=>{if(selected)setReturnId(selected);setSelected(null);setStatus('');};
  const save=()=>{if(!active)return;const note=drafts[active.id]??'';setSaved(current=>({...current,[active.id]:note}));setStatus(`${active.title}: local note snapshot saved. No source or publication changed.`);};

  return <div className="space-y-4" data-composition-ready={actualWidth!==null}>
    <div className="composition-settings">
      <label htmlFor="composition-mode">Composition<select id="composition-mode" className="mt-input" value={mode} onChange={event=>{setMode(event.target.value as Composition);setStatus('');}}><option value="list-detail">List-detail</option><option value="feed">Feed</option><option value="supporting">Supporting pane</option></select></label>
      <label htmlFor="composition-width">Maximum container width: {requestedWidth}px<input id="composition-width" type="range" min="280" max="1200" step="20" value={requestedWidth} onChange={event=>setRequestedWidth(Number(event.target.value))} /></label>
      <label className="composition-direction"><input id="composition-rtl" type="checkbox" checked={rtl} onChange={event=>setRtl(event.target.checked)} />Right-to-left placement</label>
      {mode!=='feed'&&<label htmlFor="composition-share">Leading pane share: {share}%<input id="composition-share" type="range" min="30" max="70" step="1" value={share} onChange={event=>{const value=Number(event.target.value);setShares(current=>({...current,[mode]:value}));}} aria-describedby="composition-share-help" /></label>}
    </div>
    {mode!=='feed'&&<p id="composition-share-help">Adjust the split with the native range control. The share applies only when both panes fit; it is retained while stacked. No drag handle is required.</p>}
    <p data-composition-measure>{actualWidth===null?'Measuring the available container.':`Actual container: ${Math.round(actualWidth)} CSS px. ${mode==='list-detail'?(compact?'Single pane':'Two panes'):mode==='supporting'?(actualWidth<900?'Stacked context':'Context beside task'):'Content-driven feed'}.`} The maximum is a cap, not a forced width.</p>
    <div ref={root} className="composition-container" style={{maxInlineSize:requestedWidth,'--composition-leading':`${share}fr`,'--composition-trailing':`${100-share}fr`} as CSSProperties} dir={rtl?'rtl':'ltr'} data-composition-container data-mode={mode}>
      <div className="composition-shell">
        <div className="composition-list" data-composition-list hidden={!showList}>
          <h3 dir="auto" className="font-semibold text-xl">Local example records</h3>
          <ul>{records.map(record=><li key={record.id}><h4 dir="auto" className="font-semibold">{record.title}</h4>{mode==='feed'&&<p dir="auto" lang="en">{record.description}</p>}<Button type="button" variant={selected===record.id?'primary':'outline'} ref={node=>{if(node)triggers.current[record.id]=node;else delete triggers.current[record.id];}} data-composition-open={record.id} aria-current={selected===record.id?'true':undefined} onClick={()=>choose(record.id)}>Open {record.title}</Button></li>)}</ul>
        </div>
        <div className="composition-task" data-composition-task hidden={!showDetail}>
          {mode==='supporting'&&<label htmlFor="composition-record">Active record<select id="composition-record" className="mt-input" value={active?.id??'a'} onChange={event=>{setSelected(event.target.value as RecordId);setStatus('');}}>{records.map(record=><option value={record.id} key={record.id}>{record.title}</option>)}</select></label>}
          <h3 ref={detail} dir="auto" tabIndex={-1} className="font-semibold text-xl" data-composition-heading>{active?active.title:'Choose a local record'}</h3>
          {active?<><p dir="auto" lang="en">{active.description}</p>
            {mode!=='supporting'&&<p data-composition-context dir="auto" lang="en">{active.context}</p>}
            <label htmlFor="composition-note">Local note for {active.title}<textarea id="composition-note" dir="auto" lang="" className="mt-textarea mt-textarea-resize-vertical" value={drafts[active.id]??''} onChange={event=>setDrafts(current=>({...current,[active.id]:event.target.value}))} /></label>
            <div className="composition-actions"><Button type="button" data-composition-save onClick={save}>Save local note snapshot</Button>{mode!=='supporting'&&<Button type="button" variant="outline" data-composition-back onClick={back}>{mode==='feed'?'Close detail':compact?'Back to records':'Clear selection'}</Button>}</div>
            <p data-composition-saved dir="auto" className="whitespace-pre-wrap">{saved[active.id]===undefined?'No local snapshot for this record.':<>Saved note: <bdi lang="">{saved[active.id]||'(empty note)'}</bdi></>}</p>
          </>:<p>Select a record to inspect its description and edit its own local note.</p>}
        </div>
        {mode==='supporting'&&active&&<aside className="composition-context" aria-label={`Context for ${active.title}`} data-composition-support><h3 dir="auto" className="font-semibold">Context for {active.title}</h3><p dir="auto" lang="en">{active.context}</p><p dir="auto" lang="en">The context follows its task in DOM order and stays available when stacked.</p></aside>}
      </div>
    </div>
    <p role="status" data-composition-status>{status}</p>
    <p className="text-sm">All notes and snapshots belong to this page instance. They survive composition, size and record changes, but not reload. English fixture text keeps its language and glyph order when placement is RTL. This is not a foldable-device, browser-history or remote-storage implementation.</p>
  </div>;
}

Container CSS

css
.composition-settings { display:flex; flex-wrap:wrap; align-items:end; gap:16px; }
.composition-settings label { display:flex; flex-direction:column; gap:8px; max-inline-size:100%; }
.composition-settings input[type=range], .composition-settings select { min-block-size:44px; }
.composition-settings .composition-direction { flex-direction:row; align-items:center; min-block-size:44px; }
.composition-container { container-type:inline-size; inline-size:100%; min-inline-size:0; }
.composition-shell { display:grid; gap:16px; min-inline-size:0; }
.composition-shell > [hidden] { display:none; }
.composition-list, .composition-task, .composition-context { min-inline-size:0; padding:16px; border:1px solid var(--color-muted-foreground); border-radius:var(--radius-md); }
.composition-shell p, .composition-shell h3, .composition-shell h4, .composition-shell label { overflow-wrap:anywhere; }
.composition-list ul { display:grid; gap:16px; list-style:none; margin:16px 0 0; padding:0; }
.composition-list li, .composition-task, .composition-context { display:flex; flex-direction:column; gap:12px; }
.composition-task label { display:flex; flex-direction:column; gap:8px; }
.composition-task textarea { inline-size:100%; min-block-size:120px; }
.composition-actions { display:flex; flex-wrap:wrap; gap:12px; }
.composition-shell .mt-btn { min-block-size:44px; block-size:auto; max-inline-size:100%; white-space:normal; overflow-wrap:anywhere; }
.composition-task h3 { scroll-margin-block-start:96px; }
.composition-container[data-mode=feed] .composition-list li { padding-block-end:16px; border-block-end:1px solid var(--color-muted-foreground); }
@container (min-width:640px) {
  .composition-container[data-mode=list-detail] .composition-shell { grid-template-columns:minmax(0,var(--composition-leading)) minmax(0,var(--composition-trailing)); }
  .composition-container[data-mode=feed] .composition-list ul { grid-template-columns:repeat(2,minmax(0,1fr)); }
}
@container (min-width:900px) {
  .composition-container[data-mode=supporting] .composition-shell { grid-template-columns:minmax(0,var(--composition-leading)) minmax(0,var(--composition-trailing)); }
  .composition-container[data-mode=feed] .composition-list ul { grid-template-columns:repeat(3,minmax(0,1fr)); }
}
@media (forced-colors:active) {
  .composition-list, .composition-task, .composition-context, .composition-list li { border-color:CanvasText; }
}

Verification

  • At widths below/above 640 and 900px, compare reported container width to measured bounds and visible pane relationships. A large viewport with a narrow container must still use the compact behavior.
  • Open A and B, keep separate drafts/snapshots, change modes and resize repeatedly; verify exact values and no duplicate task effect.
  • Use Tab/Enter to open, edit, save and return. Test focus when a selected list item disappears during resize.
  • Change the leading-pane share with arrow keys; check limits, dormant preference preservation, RTL geometry and surviving targets.
  • Inspect feed tracks, supporting context and list/detail at 320px, both themes, enlarged text and long unbroken content.
  • Review actual mobile/desktop input, software keyboard, browser zoom and any fold/spatial hardware before making platform claims.
  • Verify addressable selection and browser history separately when those are requirements of the consuming application.