MobTranslate styleguide

Spacing

Show relationships through proximity, preserve reading rhythm and leave enough room to operate controls.

Proximity communicates grouping

Use smaller gaps within a group than between groups. A field label, input and help text form one unit. A new section needs more separation than the next paragraph. Do not rely on an arbitrary empty box to create structure.

RelationshipStarting tokensDecision
Label to control--spacing-2Keep the label visually attached to its field.
Items in a control group--spacing-2 or --spacing-3Allow focus rings and pointer separation.
Related content blocks--spacing-4 or --spacing-6Preserve the definition/source relationship.
Separate sections--spacing-8 or --spacing-12Make a new topic legible without adding a card around everything.
Page edge--spacing-4 compact; --spacing-8 widerIncrease gutters when space permits; preserve compact content width.

Scale specifications

These values are generated from CSS. Fractional steps support small alignment adjustments; larger steps establish rhythm. Use the existing scale before adding a new value.

TokenDefault value
--spacing-00px
--spacing-0-50.125rem
--spacing-10.25rem
--spacing-1-50.375rem
--spacing-102.5rem
--spacing-123rem
--spacing-164rem
--spacing-20.5rem
--spacing-2-50.625rem
--spacing-205rem
--spacing-246rem
--spacing-30.75rem
--spacing-41rem
--spacing-51.25rem
--spacing-61.5rem
--spacing-82rem

Live grouping lab

Change padding, gap, page margin, direction, density and text size independently. The readout uses actual computed styles and action rectangles. Extreme token combinations may need the labelled specimen’s horizontal scroll; they are inspectable failures, not recommended compact layouts. A token is a distance, not a guarantee of adequate targets.

No source record selected. No data is submitted.

Dashed outline: page boundary. Solid outline: component. The page boundary includes 4 CSS px of focus clearance. Oversized combinations can be scrolled within this labelled region. The unfilled distance outside the component is margin; internal edge space is padding; sibling space is gap. Wrapping preserves the selected gap. Leading and trailing swap physical sides with direction.

Current spacing mappings and scale boundary
gap: var(--spacing-4);
padding-inline-start: var(--spacing-4);
padding-inline-end: var(--spacing-4);
padding-block: var(--spacing-4);
margin-inline: var(--spacing-2);
font-size: 100%;

The specimen text-size selector changes text independently of rem-based spacing. Changing the document root font size scales this library’s rem tokens too; this is different from fixed native dp spacing. The readout measures this rendered specimen, not every consuming component.

Native scale and the local web contract

The current Material reference uses an 8dp base unit named md.sys.measurement.space100, with main multiples and selected nested values such as 2, 4, 6 and 10dp. A new space225 would mean 8 × 2.25 = 18dp. It is an extensible scale rather than a requirement to define every possible increment.

MobTranslate uses generated --spacing-* CSS properties, mostly in rem, with a four-CSS-pixel base step at the default 16px root. A suffix identifies the local step, not a Material percentage or physical measurement. For example, --spacing-2 is .5rem and --spacing-4 is 1rem. Native dp, CSS px, rem and physical display pixels are different units; do not rename one catalogue into another without an explicit conversion policy.

Captured on 2026-10-14: Material lists this spacing system as available in Jetpack Compose and unavailable for Web and Android Views, with component mapping still in progress. This guide implements the existing web library’s contract and does not claim native SDK spacing support.

Component parts and page arrangements

Component padding separates text/icons/actions from the component edge. Sibling gaps separate content parts within it. Page margins and spacers arrange panes and components relative to the page boundary. Define leading/trailing positions logically, so they exchange physical sides in RTL.

A uniform parent gap is appropriate for equal sibling relationships. In a complex component, name the relationship between parts, such as icon-label gap, before sharing a value. Parent padding and gap usually preserve wrap/reorder behavior more clearly than separate margins on every child.

State can change one relationship independently: the reference search example changes external horizontal margin from 24dp to 12dp on focus while leaving its 8dp vertical padding and horizontal gap unchanged. That is a native reference illustration, not this library’s search default or an instruction to make every focused web control move.

Apply system values through component attributes

Use the established component recipe first, then trace its actual bindings in Component Specifications. Some values are token-bound, some are recipe choices, and imported JavaScript presets are snapshots rather than automatic CSS overrides. Preserve the full default/state contract when changing a binding.

For custom web components, map shared system values to explicit padding, margin and gap attributes. Prefer positional names such as leading padding or vertical gap to an ambiguous leading-space label that could mean internal or external space. Keep the mapping near the component rather than assuming one global density switch rewrites every recipe.

Customize mappings, patterns and density

The specimen’s compact setting changes only its vertical padding mapping from --spacing-4 to --spacing-2. It is an explicit local demonstration, not a density API for every exported component. Layout Compositions demonstrates adaptive container-based arrangements separately.

ScopeChangeReview
Product-wide component defaultChange the component padding binding to an existing system step.Check all uses, states, target dimensions and migrations.
Shared product patternName a repeated surface-content padding relationship.Share only if meaning and adaptation are actually identical.
New system stepAdd a reviewed value to the actual stylesheet and regenerate.Record purpose, units and API ownership; avoid arbitrary aliases.
Adaptive layoutMap component/page attributes using the available container.Preserve content and actions across each threshold.
Density settingReduce a specific component’s vertical padding.Retain usable action targets, reading rhythm and focus boundaries.

Text scaling and retained spacing

Material recommends retaining spacing by default while text scales to 200%. Text and spacing are separate decisions: larger text can increase a component’s natural height without multiplying every gap or padding value.

The specimen text-size selector changes inherited text size while its rem token mappings stay constant. Its buttons grow and wrap rather than clipping at a fixed height. Changing the document root font size also scales this web library’s rem-based spacing; browser zoom scales the presentation again. That unit behavior is intentional here and differs from keeping a native dp distance fixed.

Check text-only enlargement, root-size changes and browser zoom separately. Density must not silently shrink action targets to compensate for longer or larger content.

Internal padding and external gaps

  • Padding defines a surface or hit area; gap separates siblings. Choose them independently.
  • Use flex/grid gap rather than margin on every child, so wrapping and reordered content preserve relationships.
  • Use logical padding-inline and margin-block to support different directions.
  • Compact tables may use tighter row padding while action buttons retain usable target areas.

Implementation

css
.field-group { display: grid; gap: var(--spacing-2); }
.form-sections { display: grid; gap: var(--spacing-8); }
.action-row { display: flex; flex-wrap: wrap; gap: var(--spacing-3); }
.content-surface { padding: var(--spacing-6); }

Failure cases

  • Avoid negative margins that hide focus outlines or overlap hit areas.
  • Avoid using spaces or repeated line breaks for visual alignment.
  • Avoid large fixed gaps that push recovery actions out of view on a narrow screen.
  • Do not remove all separation at compact widths. Preserve hierarchy while reducing page gutters.

Verification

  • Inspect the same group at 320px, a wide viewport and with twice the text length.
  • Tab through closely packed actions and confirm the complete focus outline is visible.
  • Inspect target rectangles, not just visible icon size. Test touch operation and accidental adjacent activation.
  • Use asymmetric leading/trailing padding and switch specimen direction; actual physical sides should swap while the logical values stay the same.
  • Change text size to 200%; gap and padding should stay constant for that local control while action height/wrapping adapts.
  • Inspect the labelled specimen scroll region with keyboard focus for deliberately oversized token combinations; the page itself must remain bounded.

Compiled spacing explorer

tsx
'use client';

import {useEffect, useRef, useState} from 'react';
import type {CSSProperties} from 'react';
import catalogue from '../content/tokens.json';
import './spacing-explorer.css';

const tokens=catalogue.tokens.filter(token=>token.name.startsWith('--spacing-')).sort((a,b)=>parseFloat(a.value)-parseFloat(b.value));

export function SpacingExplorer(){
  const [gap,setGap]=useState('--spacing-4');
  const [leading,setLeading]=useState('--spacing-4');
  const [trailing,setTrailing]=useState('--spacing-4');
  const [margin,setMargin]=useState('--spacing-2');
  const [compact,setCompact]=useState(false);
  const [rtl,setRtl]=useState(false);
  const [scale,setScale]=useState('100');
  const [long,setLong]=useState(false);
  const [selected,setSelected]=useState(false);
  const [measured,setMeasured]=useState<Record<string,string>>({});
  const preview=useRef<HTMLDivElement>(null);
  const block=compact?'--spacing-2':'--spacing-4';
  useEffect(()=>{
    const element=preview.current;
    if(!element)return;
    const update=()=>{
      const styles=getComputedStyle(element);
      const button=element.querySelector('button');
      const rect=button?.getBoundingClientRect();
      const next={gap:styles.gap,leading:styles.paddingInlineStart,trailing:styles.paddingInlineEnd,block:styles.paddingBlockStart,margin:styles.marginInlineStart,font:styles.fontSize,target:rect?`${rect.width.toFixed(1)} × ${rect.height.toFixed(1)} CSS px`:'Not measured'};
      setMeasured(previous=>JSON.stringify(previous)===JSON.stringify(next)?previous:next);
    };
    update();
    const observer=new ResizeObserver(update);observer.observe(element);
    element.querySelectorAll('button').forEach(button=>observer.observe(button));
    const root=new MutationObserver(update);root.observe(document.documentElement,{attributes:true,attributeFilter:['class','style']});
    return()=>{observer.disconnect();root.disconnect();};
  },[gap,leading,trailing,margin,compact,rtl,scale,long]);
  const select=(label:string,value:string,set:(value:string)=>void,name:string)=><label>{label}<select className="mt-input" data-spacing-control={name} value={value} onChange={event=>set(event.target.value)}>{tokens.map(token=><option key={token.name} value={token.name}>{token.name} · {token.value}</option>)}</select></label>;
  const style={'--demo-gap':`var(${gap})`,'--demo-leading':`var(${leading})`,'--demo-trailing':`var(${trailing})`,'--demo-block':`var(${block})`,'--demo-margin':`var(${margin})`,fontSize:`${scale}%`} as CSSProperties;
  return <div className="spacing-lab" data-spacing-lab>
    <div className="spacing-controls">
      {select('Spacing token · sibling gap',gap,setGap,'gap')}
      {select('Leading padding',leading,setLeading,'leading')}
      {select('Trailing padding',trailing,setTrailing,'trailing')}
      {select('Page margin · both inline edges',margin,setMargin,'margin')}
      <label>Specimen text size<select className="mt-input" data-spacing-scale value={scale} onChange={event=>setScale(event.target.value)}><option value="100">100%</option><option value="150">150%</option><option value="200">200%</option></select></label>
    </div>
    <label className="spacing-check"><input data-spacing-density type="checkbox" checked={compact} onChange={event=>setCompact(event.target.checked)} />Compact vertical padding · action targets retain a 44 CSS px minimum</label>
    <label className="spacing-check"><input data-spacing-rtl type="checkbox" checked={rtl} onChange={event=>setRtl(event.target.checked)} />Right-to-left specimen layout</label>
    <label className="spacing-check"><input data-spacing-long type="checkbox" checked={long} onChange={event=>setLong(event.target.checked)} />Long action labels</label>
    <div className="spacing-page-boundary" role="region" aria-label="Scrollable spacing specimen" tabIndex={0}><div data-spacing-preview ref={preview} dir={rtl?'rtl':'ltr'} style={style}>
      <button data-spacing-action type="button" aria-pressed={selected} onClick={()=>setSelected(value=>!value)}><span dir="auto">{long?'Select this source record for a later comparison':'Select source record'}</span></button>
      <button type="button" onClick={()=>setSelected(false)}><span dir="auto">{long?'Clear the current local comparison selection':'Clear selection'}</span></button>
    </div></div>
    <p role="status">{selected?'Source record selected locally.':'No source record selected.'} No data is submitted.</p>
    <dl data-spacing-measured>{Object.entries(measured).map(([name,value])=><div key={name}><dt>{name}</dt><dd>{value}</dd></div>)}</dl>
    <p>Dashed outline: page boundary. Solid outline: component. The page boundary includes 4 CSS px of focus clearance. Oversized combinations can be scrolled within this labelled region. The unfilled distance outside the component is margin; internal edge space is padding; sibling space is gap. Wrapping preserves the selected gap. Leading and trailing swap physical sides with direction.</p>
    <details><summary>Current spacing mappings and scale boundary</summary><pre><code>{`gap: var(${gap});\npadding-inline-start: var(${leading});\npadding-inline-end: var(${trailing});\npadding-block: var(${block});\nmargin-inline: var(${margin});\nfont-size: ${scale}%;`}</code></pre><p>The specimen text-size selector changes text independently of rem-based spacing. Changing the document root font size scales this library’s rem tokens too; this is different from fixed native dp spacing. The readout measures this rendered specimen, not every consuming component.</p></details>
  </div>;
}

Compiled spacing styles

css
.spacing-lab { display:grid; gap:1rem; min-inline-size:0; }
.spacing-controls { display:grid; gap:1rem; grid-template-columns:repeat(auto-fit,minmax(min(100%,16rem),1fr)); }
.spacing-controls label { min-inline-size:0; }
.spacing-lab .mt-input { display:block; inline-size:100%; margin-block-start:.5rem; }
.spacing-check { display:flex; align-items:center; gap:.75rem; min-block-size:44px; }
.spacing-check input { flex:0 0 auto; }
.spacing-lab p, .spacing-lab label, .spacing-lab summary, .spacing-lab dd { overflow-wrap:anywhere; }
.spacing-page-boundary { border:1px dashed var(--color-muted-foreground); min-inline-size:0; overflow:auto; padding:4px; scroll-padding:4px; }
.spacing-lab [data-spacing-preview] { display:flex; flex-wrap:wrap; gap:var(--demo-gap); padding-inline-start:var(--demo-leading); padding-inline-end:var(--demo-trailing); padding-block:var(--demo-block); margin-inline:var(--demo-margin); border:1px solid var(--color-border); background:var(--color-card); color:var(--color-card-foreground); min-inline-size:0; }
.spacing-lab [data-spacing-preview] button { min-inline-size:0; max-inline-size:100%; min-block-size:44px; padding:.5em; border:1px solid currentColor; border-radius:var(--radius-sm); font:inherit; text-align:start; overflow-wrap:anywhere; }
.spacing-lab [data-spacing-preview] button[aria-pressed=true] { background:var(--color-primary); color:var(--color-primary-foreground); }
.spacing-lab [data-spacing-measured] { display:grid; grid-template-columns:repeat(auto-fit,minmax(min(100%,10rem),1fr)); gap:.75rem; }
.spacing-lab dt { font-weight:600; }
.spacing-lab dd { margin:0; }
.spacing-lab summary { min-block-size:44px; }
.spacing-lab pre { white-space:pre-wrap; overflow-wrap:anywhere; }
@media(forced-colors:active) { .spacing-page-boundary,.spacing-lab [data-spacing-preview] { border-color:CanvasText; } .spacing-lab [data-spacing-preview] button[aria-pressed=true] { background:Highlight; color:HighlightText; } }