MobTranslate styleguide

Stack

Arrange existing content along one flex axis with explicit spacing, wrapping and source order.

Layout and task semantics

Stack supplies flex layout, not navigation or selection. Choose column for a reading sequence and row when content benefits from side-by-side placement. Prefer wrap when a row must fit a changing container; large two-dimensional layouts may need CSS Grid instead.

Keep DOM order meaningful for reading and keyboard focus. RTL changes physical flow through native direction; it does not justify reversing the source array or inventing toolbar keys. The example’s pressed buttons own their own local preview selection.

Anatomy and spacing contract

PartActual contract
RootNative div/ref with display:flex, min-width:0 and maximum available width. Emitted mt-stack-row/column/wrap classes now have matching CSS.
GapNumeric n means n × 0.25rem, default 3=0.75rem. String uses a CSS gap value such as var(--spacing-4) or clamp(...). This API unit is not a pixel count or an automatic token identifier.
Alignmentalign controls the cross axis; justify distributes the main axis. Row and column change which physical axis those mean.
ChildrenGive shrinkable content an explicit min-width:0. Stack preserves child minimum widths; essential controls still need meaningful targets and readable labels.
Wrappingwrap=true adds flex-wrap:wrap. Unwrapped rows can overflow if their content cannot shrink; choose an explicit scrolling or adaptation policy.
Style precedenceComputed gap/alignment are defaults; explicit style overrides them. The old implementation spread style first and then erased custom alignment with undefined props.

Working layout and actual content

Switch row/column, wrapping, numeric gap and RTL, then choose a local task. Actual English-only preview content changes while the button order remains stable. HStack lays out the controls; VStack owns the surrounding vertical sequence. All action labels wrap inside minimum targets.

Actual English-only overview content.

These pressed buttons select one local preview. Stack supplies layout only; it adds no toolbar, tab or arrow-key model.

tsx
'use client';
import {useState,type CSSProperties} from 'react';
import {Stack,HStack,VStack,AspectRatio,Separator,VisuallyHidden,Button} from '@mobtranslate/ui';
const action:CSSProperties={minWidth:0,maxWidth:'100%',minHeight:44,height:'auto',whiteSpace:'normal',overflowWrap:'anywhere'};
export function StackLayoutExample(){
  const [row,setRow]=useState(false);
  const [wrap,setWrap]=useState(true);
  const [gap,setGap]=useState(3);
  const [rtl,setRtl]=useState(false);
  const [selected,setSelected]=useState('overview');
  const tasks=[{id:'overview',label:'Local overview',text:'Actual English-only overview content.'},{id:'notes',label:'Local reading notes',text:'Actual English-only reading-note content.'},{id:'saved',label:'Local saved examples',text:'Actual English-only saved-example content.'}];
  return <VStack data-stack-lab gap={4}>
    <HStack gap={3} wrap><Button style={action} type="button" variant="outline" aria-pressed={row} onClick={()=>setRow(v=>!v)}>Use row task layout</Button><Button style={action} type="button" variant="outline" aria-pressed={wrap} onClick={()=>setWrap(v=>!v)}>Allow task wrapping</Button><Button style={action} type="button" variant="outline" aria-pressed={rtl} onClick={()=>setRtl(v=>!v)}>Use RTL task layout</Button></HStack>
    <label>Gap in quarter-rem units <select data-stack-gap className="mt-input" value={gap} onChange={event=>setGap(Number(event.target.value))}><option value={0}>0</option><option value={3}>3</option><option value={6}>6</option></select></label>
    <Stack data-stack-tasks direction={row?'row':'column'} wrap={wrap} gap={gap} align="stretch" dir={rtl?'rtl':'ltr'}>{tasks.map(task=><Button style={{...action,flex:row?'1 1 10rem':undefined}} key={task.id} type="button" variant={selected===task.id?'primary':'outline'} aria-pressed={selected===task.id} onClick={()=>setSelected(task.id)}>{task.label}</Button>)}</Stack><p data-stack-result>{tasks.find(task=>task.id===selected)?.text}</p>
    <p>These pressed buttons select one local preview. Stack supplies layout only; it adds no toolbar, tab or arrow-key model.</p>
  </VStack>;
}
export function AspectFrameExample(){
  const [ratio,setRatio]=useState(16/9);
  const [cover,setCover]=useState(false);
  return <div data-aspect-lab className="space-y-4"><label>Frame width divided by height <select data-aspect-ratio className="mt-input" value={ratio} onChange={event=>setRatio(Number(event.target.value))}><option value={16/9}>16:9</option><option value={4/3}>4:3</option><option value={1}>1:1</option><option value={3/4}>3:4</option></select></label><Button style={action} type="button" variant="outline" aria-pressed={cover} onClick={()=>setCover(v=>!v)}>Crop illustration to cover frame</Button>
    <figure><AspectRatio ratio={ratio} data-aspect-frame><svg viewBox="0 0 400 200" preserveAspectRatio={cover?'xMidYMid slice':'xMidYMid meet'} role="img" aria-label="English-only geometric illustration with three numbered regions" style={{background:'var(--color-muted)'}}><rect x="0" y="0" width="400" height="200" fill="var(--color-muted)"/><rect x="20" y="20" width="100" height="160" fill="var(--color-background)" stroke="var(--color-foreground)"/><circle cx="200" cy="100" r="60" fill="var(--color-primary)"/><rect x="280" y="20" width="100" height="160" fill="var(--color-background)" stroke="var(--color-foreground)"/><text x="70" y="110" textAnchor="middle" fill="var(--color-foreground)" fontSize="32">1</text><text x="200" y="110" textAnchor="middle" fill="var(--color-primary-foreground)" fontSize="32">2</text><text x="330" y="110" textAnchor="middle" fill="var(--color-foreground)" fontSize="32">3</text></svg></AspectRatio><figcaption className="mt-3">Local geometric illustration, not a photograph. {cover?'Cover can crop edge regions.':'Contain preserves all three regions.'} Caption stays outside the clipped media frame.</figcaption></figure>
  </div>;
}
export function SeparatorSectionsExample(){
  const [vertical,setVertical]=useState(false);
  const [decorative,setDecorative]=useState(false);
  return <div data-separator-lab className="space-y-4"><HStack wrap gap={3}><Button style={action} type="button" variant="outline" aria-pressed={vertical} onClick={()=>setVertical(v=>!v)}>Use vertical section divider</Button><Button style={action} type="button" variant="outline" aria-pressed={decorative} onClick={()=>setDecorative(v=>!v)}>Make local divider decorative</Button></HStack><Stack direction={vertical?'row':'column'} gap={3} style={vertical?{minHeight:100}:undefined}><p style={{flex:1}}>English-only draft details.</p><Separator data-local-divider orientation={vertical?'vertical':'horizontal'} aria-hidden={decorative||undefined} className={state=>state.orientation==='vertical'?'divider-vertical':'divider-horizontal'} /><p style={{flex:1}}>English-only local result.</p></Stack><p>Default is a semantic separator; decorative mode hides this redundant boundary from the accessibility tree. The copy remains visible.</p></div>;
}
export function HiddenNameExample(){
  const [saved,setSaved]=useState(false);
  return <div data-hidden-lab className="space-y-4"><p>An icon control has a real text name in a visually hidden span. Visible help remains available for everyone.</p><Button id="hidden-bookmark" type="button" size="icon" variant="outline" aria-pressed={saved} onClick={()=>setSaved(v=>!v)}><svg aria-hidden="true" width="20" height="20" viewBox="0 0 24 24" fill={saved?'currentColor':'none'} stroke="currentColor"><path d="M6 3h12v18l-6-4-6 4z"/></svg><VisuallyHidden data-hidden-name>Toggle local example bookmark</VisuallyHidden></Button><p role="status" data-hidden-result>{saved?'Actual local bookmark present.':'Actual local bookmark absent.'}</p><p>Keep interactive controls outside VisuallyHidden. Clipping a focusable link would leave an invisible keyboard target.</p></div>;
}

Complete public API

Export / propBehavior
Stack.directionrow or column, default column. No automatic breakpoint or reverse ordering is provided.
gapnumber or CSS string, default 3. Numeric quarter-rem conversion is explicit; validate dynamic lengths rather than concatenating arbitrary untrusted input.
alignstart, center, end, stretch or baseline. Start/end map to flex-start/end. Omitted alignment leaves normal flex defaults or explicit style.
justifystart, center, end, between, around or evenly; maps to the corresponding flex distribution.
wrapBoolean wrapping, false/omitted by default. Does not supply a scroll region or minimum child width automatically.
HStack / VStackTyped aliases fixing row/column; accept the remaining Stack props and forward the same native div ref.
className / style / native propsNative root attributes, handlers, children and HTMLDivElement ref. Explicit style wins over computed layout defaults; preserve desired DOM order and semantic roles yourself.

Adaptation and accessibility

  • Read/Tab order follows DOM order, not visual alignment. Use native actions/links and proper group semantics for the actual task.
  • A visual row is not an ARIA toolbar. Do not add roving arrow behavior to ordinary buttons merely because they are adjacent.
  • Test long translations, text spacing and narrow containers. min-width:0 permits shrinking but does not make fixed-width descendants or nowrap labels readable.
  • Use measured available width when presentation needs to change. HStack/VStack do not own a media-query or ResizeObserver policy.
  • Alignment/distribution changes can create large empty gaps. Verify the actual computed layout and distinguish gap from space-between distribution.

Migration and verification

Run test:layout-utilities, checked-source/token freshness, lint, UI/styleguide types, production build and the complete route/fragment crawl. Verify real geometry, DOM order, accessible names and task outcomes; browser evidence does not certify every consuming layout or spoken assistive-technology/device combination.

Legacy emitted direction/wrap classes did not match their CSS. The repaired selectors now produce actual flex direction and wrapping. If a consumer relied on that accidental row layout or old style precedence, reconcile its intended composition.

  • Check computed direction/gap, row wrap, geometry/targets, unchanged source order, real action outcomes and explicit style overrides. No durable state or automatic keyboard model is implied.