MobTranslate styleguide

Card

Group a discrete item with clear content and explicit actions; use a surface only when it improves the task.

When to use a card

Use a card when one item has a coherent title, supporting content and actions. A compact list or table is often easier for comparison and scanning. Avoid nested cards and decorative surfaces around every paragraph.

This package supports filled, outlined and elevated surfaces corresponding to the Material card family, plus a default that retains the previous border/small-shadow appearance. These are visual grouping choices, not automatic button, selection or review semantics.

Anatomy, variants and specifications

PartCurrent specificationDecision
Root2px border, --radius-lg, card foreground/background, clipped contentUse a region/article semantic wrapper only when meaningful; Card remains a div.
defaultCard surface, border and --shadow-smExisting visual default; no implicit action on hover.
filledMuted surface, transparent border, no shadowGroup without suggesting elevation.
outlinedCard surface and border, no shadowSeparate related content with a boundary.
elevatedCard surface and --shadow-md; transparent borderUse spatial emphasis sparingly, not as proof of priority.
Header / content / footer24px padding; wrapping footer with 8px action gapKeep action order and content reading order consistent.
Title / descriptionh3, --text-xl weight 700 / p, --text-sm mutedChoose a suitable heading hierarchy; use child markup with package classes if h3 is unsuitable.

Material card anatomy and collection choices

The reference’s elevated, filled and outlined variants have the same task capabilities with different emphasis: a subtle filled grouping, elevated separation, or an explicit outline. The container is the only required element; title, subhead, supporting text, media, dividers and actions follow the item’s hierarchy. Do not force a card around content that spacing and headings would organize more clearly.

Native reference measurements include 12dp corners, 16dp inline padding and up to 8dp between cards. This library instead uses its actual radius, 24px part padding and caller-owned layout gap. Those native measurements are not CSS conversions or imported component presets.

Reference subjectWeb composition and boundary
Content and mediaKeep coherent blocks in reading order. Inset dividers separate related blocks; full-width divisions can clarify expanded content. Prefer text separate from imagery; any overlay needs contrast across the real image, an appropriate scrim/bounding shape and meaningful alternative text. No image or permission service is installed by Card.
Actions and semanticsChoose a non-actionable container with independently named controls, or one native whole-card link/button with no nested actions. The specimen demonstrates both separately. Native link Enter activation and Space scrolling remain different from a button’s Enter/Space activation. The root div supplies no selection, hover action or automatic tab stop.
Collections and adaptationKeep resting cards coplanar, with filter/sort controls outside the collection applying to every record. The actual collection filters titles/bookmarks, sorts titles and retains bookmarks across presentation changes. Grid columns reflow from available space; long content grows instead of clipping to a forced equal height. Related Lists/carousel guidance covers alternate presentations.
Expansion and scrollingThe actual native details disclosure grows its card in document flow. Reference mobile cards avoid nested scrolling and can expand into a screen; desktop guidance permits internal expansion/scroll. This specimen keeps one document scroll and does not install a native hero container transform or full-screen navigation transition.
Drag, swipe and stateReference picked-up cards rise above peers without pushing them aside, and allow one swipe action without nested swipe carousels. Card itself installs no drag/swipe/disabled/loading behavior. If composed, provide explicit pointer/keyboard commands, actual target/recovery state and clear focus; see Input and gestures rather than implying a styled surface implements those operations.

Working surface and explicit actions

These compositions use 16 CSS px part insets and 12 CSS px action padding, retaining room for full words when root text doubles. The package defaults remain those specified above. The whole-card destination is deliberately brief; its separate surrounding explanation remains readable in document flow.

Local reading collection

An English-only example with explicit actions.

The container itself is not clickable. Open the related guidance or change this local bookmark.

A collection with independent actions

Filters and sorting apply to every local card. Surface changes preserve bookmarks. The cards grow with their contents and do not scroll internally.

Shown 3 of 3 local cards. 0 bookmarked. No remote or durable write occurred.

Reading notes

Local reading settings and a retained English-only draft.

Read Reading notes details

This is retained local content for Reading notes. Expanding grows the card in normal document flow. It does not validate a source, authorize access or create a remote result.

Comparison notes

Local examples arranged for comparison, without review claims.

Read Comparison notes details

This is retained local content for Comparison notes. Expanding grows the card in normal document flow. It does not validate a source, authorize access or create a remote result.

Recovery notes

Local cancellation, retry and retained recovery examples.

Read Recovery notes details

This is retained local content for Recovery notes. Expanding grows the card in normal document flow. It does not validate a source, authorize access or create a remote result.

A separate, single-action card

This whole-card link has one destination and no nested controls. The collection above keeps its independent controls inside non-actionable containers.

Open notification recovery guidance

Read the pattern.

Collection is not locally bookmarked.

tsx
'use client';
import { useRef, useState } from 'react';
import { Button, Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, Input, type CardProps } from '@mobtranslate/ui';

export function CardContainmentExample() {
  const [variant, setVariant] = useState<NonNullable<CardProps['variant']>>('outlined');
  const [saved, setSaved] = useState(false);
  return <div data-card-lab className="space-y-4">
    <label>Card surface <select data-card-surface className="mt-input" value={variant} onChange={event => setVariant(event.target.value as typeof variant)}>{['default','filled','outlined','elevated'].map(item => <option key={item}>{item}</option>)}</select></label>
    <Card variant={variant} role="region" aria-labelledby="local-card-title">
      <CardHeader style={{padding:16}}><CardTitle dir="auto" id="local-card-title">Local reading collection</CardTitle><CardDescription dir="auto">An English-only example with explicit actions.</CardDescription></CardHeader>
      <CardContent style={{padding:16}}><p dir="auto">The container itself is not clickable. Open the related guidance or change this local bookmark.</p></CardContent>
      <CardFooter style={{padding:16}}><a className="mt-btn mt-btn-outline mt-btn-md" style={{paddingInline:12}} href="/patterns/notifications">Read recovery guidance</a><Button type="button" style={{paddingInline:12}} variant="secondary" aria-pressed={saved} onClick={() => setSaved(current => !current)}>{saved ? 'Remove local bookmark' : 'Bookmark locally'}</Button></CardFooter>
    </Card>
    <CardCollectionExample variant={variant} />
    <p role="status" dir="auto">{saved ? 'Local bookmark added; nothing was sent or persisted.' : 'Collection is not locally bookmarked.'}</p>
  </div>;
}


const cardRecords = [
  {id:'reading',title:'Reading notes',description:'Local reading settings and a retained English-only draft.'},
  {id:'comparison',title:'Comparison notes',description:'Local examples arranged for comparison, without review claims.'},
  {id:'recovery',title:'Recovery notes',description:'Local cancellation, retry and retained recovery examples.'},
];
function CardCollectionExample({variant}:{variant:NonNullable<CardProps['variant']>}) {
  const [query,setQuery]=useState('');
  const [onlySaved,setOnlySaved]=useState(false);
  const [saved,setSaved]=useState<Set<string>>(()=>new Set());
  const [order,setOrder]=useState('source');
  const [rtl,setRtl]=useState(false);
  const filter=useRef<HTMLInputElement>(null);
  const savedFilter=useRef<HTMLElement>(null);
  const visible=cardRecords.filter(record=>(!onlySaved||saved.has(record.id))&&record.title.toLocaleLowerCase('en').includes(query.trim().toLocaleLowerCase('en')));
  if(order!=='source')visible.sort((a,b)=>new Intl.Collator('en').compare(a.title,b.title)*(order==='az'?1:-1));
  function toggle(id:string){
    if(onlySaved&&saved.has(id))savedFilter.current?.focus();
    setSaved(previous=>{const next=new Set(previous);if(next.has(id))next.delete(id);else next.add(id);return next;});
  }
  return <section data-card-collection dir={rtl?'rtl':'ltr'} aria-labelledby="local-card-collection-title" className="space-y-4">
    <h3 dir="auto" id="local-card-collection-title" className="font-semibold">A collection with independent actions</h3>
    <p dir="auto">Filters and sorting apply to every local card. Surface changes preserve bookmarks. The cards grow with their contents and do not scroll internally.</p>
    <label className="block" htmlFor="local-card-filter">Filter local card titles</label><Input dir="auto" ref={filter} id="local-card-filter" value={query} onChange={event=>setQuery(event.target.value)} />
    <div className="flex flex-wrap gap-3">
      <Button ref={savedFilter} type="button" variant="outline" aria-pressed={onlySaved} onClick={()=>setOnlySaved(value=>!value)}>Bookmarked cards only</Button>
      <Button type="button" variant="outline" aria-pressed={rtl} onClick={()=>setRtl(value=>!value)}>Use RTL card collection</Button>
      <label>Local card order <select aria-label="Local card order" className="mt-input" value={order} onChange={event=>setOrder(event.target.value)}><option value="source">Source order</option><option value="az">Title A–Z</option><option value="za">Title Z–A</option></select></label>
    </div>
    <p dir="auto" role="status" data-card-collection-status>Shown {visible.length} of {cardRecords.length} local cards. {saved.size} bookmarked. No remote or durable write occurred.</p>
    {visible.length ? <div data-card-records style={{display:'grid',gridTemplateColumns:'repeat(auto-fit,minmax(min(100%,16rem),1fr))',gap:16,alignItems:'start'}}>{visible.map(record=><Card key={record.id} variant={variant} role="article" aria-labelledby={`local-card-${record.id}`} data-card-id={record.id}>
      <CardHeader style={{padding:16}}><h4 dir="auto" id={`local-card-${record.id}`} className="mt-card-title">{record.title}</h4></CardHeader>
      <CardContent style={{padding:16}}><p dir="auto">{record.description}</p><details className="mt-4"><summary style={{minHeight:44,cursor:'pointer'}}>Read {record.title} details</summary><p className="mt-3">This is retained local content for {record.title}. Expanding grows the card in normal document flow. It does not validate a source, authorize access or create a remote result.</p></details></CardContent>
      <CardFooter style={{padding:16}}><Button type="button" style={{paddingInline:12}} variant="secondary" aria-pressed={saved.has(record.id)} onClick={()=>toggle(record.id)}>{saved.has(record.id)?'Remove bookmark':'Bookmark'} {record.title}</Button></CardFooter>
    </Card>)}</div> : <div data-card-empty><p dir="auto">No local card titles match these filters. Bookmarks remain retained.</p><Button type="button" onClick={()=>{setQuery('');setOnlySaved(false);filter.current?.focus();}}>Show all local cards</Button></div>}
    <h3 dir="auto" className="font-semibold">A separate, single-action card</h3>
    <p dir="auto">This whole-card link has one destination and no nested controls. The collection above keeps its independent controls inside non-actionable containers.</p>
    <a data-single-card href="/patterns/notifications" aria-labelledby="local-single-card-title" style={{padding:16}} className="mt-card mt-card-outlined block focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-ring"><h4 dir="auto" id="local-single-card-title" className="font-semibold underline">Open notification recovery guidance</h4><p dir="auto">Read the pattern.</p></a>
  </section>;
}

API reference

Export / propContract
Card / CardPropsHTML div props/ref plus variant=default|filled|outlined|elevated; default default.
CardHeader / CardContent / CardFooterDiv props/ref; structural spacing, not header/main/footer landmarks.
CardTitleh3 props and HTMLHeadingElement ref.
CardDescriptionp props and HTMLParagraphElement ref.
className / style / childrenMerged classes, native styles and caller-owned content.
Interaction / mediaNo built-in onSelect, loading, disabled, image or permission API. Compose explicit controls and source-backed media.

Behavior and accessibility

  • The root is read-only by default and must not become an unexplained Tab stop. Use a real link for navigation and a button for actions.
  • Keep separate actions separate. Do not put buttons inside a whole-card link or nest interactive controls.
  • If an application needs a single linked card, make its one link understandable from the title and avoid duplicate destinations. If selection is needed, compose a named checkbox or radio with actual selected state.
  • For media, provide a useful alternative or mark decorative imagery appropriately. Confirm rights, identification and locality before using real cultural/resource imagery.
  • Retain long titles, empty/error/loading messages and action visibility at narrow widths. Clipped overflow must not crop essential focus indicators.
  • No unconditional elevation-on-hover is applied: a static container should not suggest a click that has no outcome.

Failure cases

  • A div with a click handler but no keyboard control or accessible action name.
  • A decorative surface mistaken for source verification or qualified review.
  • An entire card link containing another button/link.
  • A disabled card whose inner controls still act, or a skeleton with no error/recovery outcome.
  • Long footer actions overflowing because the layout assumes one short label.

Verification and release scope

  • Verify the actual title, description relationships, focus destination, keyboard operation and task result in the consuming application.
  • Exercise narrow and short viewports, both themes, long labels, text spacing, reduced motion and forced colors. Reach the last action by scrolling and keyboard.
  • For overlays, check background interaction, scroll locking and each permitted dismissal path. A role or aria-modal attribute alone does not prove modal behavior.
  • Use a named screen reader and browser for spoken output. Local examples and automated DOM checks do not prove announcements or a persisted backend result.
  • Retain source/review/permission evidence. A container or confirmation cannot grant authority or establish that a request succeeded.