MobTranslate styleguide

Chip

Use concise actions, filter states, suggestions and removable inputs with the semantics each role requires.

Choose the chip role

RoleExport / patternActual meaning
Read-only labelChipMetadata; not an action or filter.
Assist actionChipActionNamed native button with an actual outcome.
SuggestionChipActionAction adds an actual local input/choice; no invented language data.
FilterChipTogglePressed toggle with a stable filter name and a real changed result list.
Input tokenChip with onDelete/deleteLabelIndependent edit/removal buttons in the working input example, with a retained-record restore path.

Anatomy, variants and sizes

PropertyCurrent contract
Variantsfilled: semantic/muted fill; outlined: transparent readable text and tone border; soft: tinted surface with readable foreground
Colorsdefault | primary | success | warning | error; text remains meaningful
Sizessm: 2px/8px padding, text-xs; md: 4px/12px, text-sm; lg: 8px/16px, text-base
Shape--radius-full; 1.5px border; long labels wrap inside max-width 100%
Interactive targetChipAction/ChipToggle min-height 44px; removal button 44×44px
Static chipContent-sized div; does not claim a 44px action target
FocusExplicit token outline on chip actions/removal; unavailable actions use native/primitive guards

Material variants, input ownership and accessible overflow

Material distinguishes contextual assist actions, filters for a collection, user-authored input tokens and product-authored suggestions. They supplement the current task; use a persistent Button for significant progress/final submission. The local demo separates action and filter groups, labels their purpose, combines its two filters explicitly and provides an empty-result recovery. Suggestions add a fixed English-only fixture; the named input form can add or edit actual token records from entered text. Edit and removal are independent buttons, additions retain other records, and restore recovers the most recently removed token without duplicating exact text.

The reference recommends labels of about 20 characters or fewer, generally verb-first for assist actions and positive categories/noun phrases for filters/suggestions. This is guidance, not an HTML input length limit. Local tokens retain their full entered label and grow/wrap at enlarged text instead of silently truncating user input. Concise contextual copy, a group label and a visible boundary help show that the interactive variants are operable; a static Chip remains metadata.

Reference pointLocal treatment
Parts and geometryReference container/label, optional leading icon/image and trailing action; baseline 32dp height/8dp corners/18dp icons, with 24dp input avatars and native padding variants. This library has pill corners and sm/md/lg padding/type specifications above; it does not supply native avatar/elevation/configuration APIs.
Targets and spacingReference targets are 48dp (48×48 CSS px in the web density guidance), with 8dp spacing and independent trailing-action clearance such as an 88dp container. Local actions have 44px minimum heights and removal has its own 44×44px target. The demo wraps with a 12 CSS px gap; it never nests a removal button inside another action.
Overflow and discoverabilityThe demo shows all options by wrapping. If a consuming product uses horizontal overflow, add a Show all/reflow action or a menu for one-action chips. Do not hide two-action/remove controls in a menu merely to avoid overflow. A real keyboard-focusable scroll region still needs a way to discover all options.
Selection and keyboardLocal filters are independent pressed buttons reached with Tab, not an invented grid. Material’s web accessibility table lists gridcell for some chip sets; that role requires a complete grid owner and arrow model. Use the actual composite model if implementing one, rather than assigning gridcell to isolated buttons.
Removal and editingEnter/Space activate the named remove button; unmodified Delete/Backspace on that button also remove the token. Editing occurs in the explicit text input, whose Backspace remains normal text editing. This package does not implement inline caret-to-token selection, two-step field deletion, dragging/reordering or native container-transform expansion.
Contrast and elevationThe reference mentions 3:1 chip-label contrast and interactive outline cues; this site requires at least 4.5:1 for ordinary chip text and 3:1 for meaningful graphics. It uses flat surfaces/boundaries and state cues, not elevated chip overlays on images or a pressed ripple engine.

Working variants, filtering, suggestion and recovery

default local label
primary local label
success local label
warning local label
error local label

Select local filters. Both filters apply together.

Editing an existing local chip. Exact duplicate text is not added twice.

  • Local reading sample
  • Local note sample

No local chip action yet. Local filter: all bookmarks; all kinds. 2 matching local examples.

tsx
'use client';
import { useRef, useState } from 'react';
import { Button, Chip, ChipAction, ChipToggle, Input, type ChipProps } from '@mobtranslate/ui';

export function ChipSelectionExample() {
  const [variant, setVariant] = useState<NonNullable<ChipProps['variant']>>('filled');
  const [size, setSize] = useState<NonNullable<ChipProps['size']>>('md');
  const [filtered, setFiltered] = useState(false);
  const [notesOnly,setNotesOnly]=useState(false);
  const [help,setHelp]=useState(false);
  const [draft,setDraft]=useState('English-only local suggestion');
  const [tokens,setTokens]=useState([{id:0,label:'English-only local suggestion'}]);
  const [editing,setEditing]=useState<number|null>(0);
  const [removed,setRemoved]=useState<{id:number;label:string}|null>(null);
  const nextToken=useRef(0);
  const [error,setError]=useState('');
  const entry=useRef<HTMLInputElement>(null);
  const visible=[{id:'reading',title:'Local reading sample',bookmarked:true,note:false},{id:'note',title:'Local note sample',bookmarked:false,note:true}].filter(item=>(!filtered||item.bookmarked)&&(!notesOnly||item.note));
  const [result, setResult] = useState('No local chip action yet.');
  const restore = useRef<HTMLElement>(null);
  return <div data-chip-lab className="space-y-4">
    <label>Chip variant <select data-chip-variant className="mt-input" value={variant} onChange={event => setVariant(event.target.value as typeof variant)}>{['filled','outlined','soft'].map(item => <option key={item}>{item}</option>)}</select></label>
    <label>Chip size <select data-chip-size className="mt-input" value={size} onChange={event => setSize(event.target.value as typeof size)}>{['sm','md','lg'].map(item => <option key={item}>{item}</option>)}</select></label>
    <div className="flex flex-wrap gap-3">{(['default','primary','success','warning','error'] as const).map(color => <Chip key={color} color={color} variant={variant} size={size} data-tone={color}>{color} local label</Chip>)}</div>
    <div role="group" aria-label="Contextual local actions" className="flex flex-wrap gap-3"><ChipAction onClick={() => {setHelp(true);setResult('Local filter explanation is visible below.');}} variant={variant} size={size}>Explain local filter</ChipAction><ChipAction onClick={() => { if(!tokens.some(item=>item.label==='English-only local suggestion')){const id=++nextToken.current;setTokens(items=>[...items,{id,label:'English-only local suggestion'}]);}setError('');setResult('English-only local suggestion is present. Existing input chips are retained.'); }} variant={variant} size={size}>Add local suggestion</ChipAction><ChipAction disabled>Unavailable local action</ChipAction></div>
    {help&&<p data-chip-help>Bookmarked examples includes retained bookmarks. Notes examples includes notes. Selecting both requires both conditions; clear filters to recover the complete local list.</p>}
    <p>Select local filters. Both filters apply together.</p>
    <div role="group" aria-label="Select local filters" className="flex flex-wrap gap-3"><ChipToggle pressed={filtered} onPressedChange={setFiltered} variant={variant} size={size}>Bookmarked examples</ChipToggle><ChipToggle pressed={notesOnly} onPressedChange={setNotesOnly} variant={variant} size={size}>Notes examples</ChipToggle></div>
    <Button type="button" variant="outline" aria-disabled={!filtered&&!notesOnly||undefined} onClick={()=>{setFiltered(false);setNotesOnly(false);}}>Clear local chip filters</Button>
    <form className="space-y-3" noValidate onSubmit={event=>{event.preventDefault();if(!draft.trim()){setError('Enter text for this local chip.');entry.current?.focus();return;}const label=draft.trim();if(tokens.some(item=>item.id!==editing&&item.label===label)){setError('A local chip already uses that exact text.');entry.current?.focus();return;}const existing=tokens.find(item=>item.id===editing);const id=existing?.id??++nextToken.current;setTokens(items=>existing?items.map(item=>item.id===id?{id,label}:item):[...items,{id,label}]);setEditing(id);setError('');setResult('Local input chip updated from the entered text. Other chips are retained.');}}>
      <label htmlFor="local-chip-text">Local chip text</label><Input ref={entry} id="local-chip-text" value={draft} onChange={event=>{setDraft(event.target.value);setError('');}} aria-invalid={!!error||undefined} aria-describedby={error?'local-chip-error':undefined}/>
      {error&&<p id="local-chip-error" role="alert">{error}</p>}
      <div className="flex flex-wrap gap-3"><Button type="submit">Use local chip text</Button><Button type="button" variant="outline" onClick={()=>{setEditing(null);setDraft('');setError('');entry.current?.focus();}}>New local chip</Button></div>
      <p>{editing===null?'Entering a new local chip.':'Editing an existing local chip.'} Exact duplicate text is not added twice.</p>
    </form>
    <div role="group" aria-label="Local input chips" className="flex flex-wrap gap-3">{tokens.map(token=><Chip key={token.id} onDelete={()=>{setRemoved(token);setTokens(items=>items.filter(item=>item.id!==token.id));if(editing===token.id)setEditing(null);setResult('Local input chip removed. Restore remains available.');restore.current?.focus();}} deleteLabel={`Remove ${token.label}`} variant={variant} size={size}><button type="button" className="mt-chip-action" aria-label={`Edit ${token.label}`} style={{minHeight:44,minWidth:44,maxWidth:'100%',padding:0,border:0,background:'transparent',color:'inherit',font:'inherit',textAlign:'start',whiteSpace:'normal',overflowWrap:'anywhere',cursor:'pointer'}} onClick={()=>{setEditing(token.id);setDraft(token.label);setError('');entry.current?.focus();}}>{token.label}</button></Chip>)}</div>
    <Button ref={restore} style={{minHeight:44,height:"auto",minWidth:0,maxWidth:"100%",whiteSpace:"normal",overflowWrap:"anywhere"}} type="button" variant="outline" aria-disabled={!removed||undefined} onClick={()=>{if(removed){const exists=tokens.some(item=>item.id===removed.id||item.label===removed.label);if(!exists)setTokens(items=>[...items,removed]);setRemoved(null);setResult(exists?'A chip with the removed text is already present.':'Local input chip restored.');}}}>Restore local chip</Button>
    <ul aria-label="Local filtered examples">{visible.map(item => <li key={item.id}>{item.title}</li>)}</ul>
    {visible.length===0&&<p data-chip-empty>No local examples match both filters. Clear filters to show the retained examples.</p>}
    <p role="status">{result} Local filter: {filtered ? 'bookmarked examples' : 'all bookmarks'}; {notesOnly?'notes examples':'all kinds'}. {visible.length} matching local examples.</p>
  </div>;
}

API reference

Export / propContract
Chip / ChipPropsDiv props/ref; variant=filled default, color=default, size=md.
onDelete / deleteLabelCaller removal callback; default Remove chip. Native click/Enter/Space and unmodified Delete/Backspace on the remove button call it. Name the specific token and choose final focus.
ChipAction / ChipActionPropsNative button props/ref with the same appearance props; type=button default; disabled supported.
ChipToggle / ChipTogglePropsInstalled Toggle props/ref plus appearance; variant=outlined default. pressed/defaultPressed/onPressedChange/disabled retain real toggle semantics.
className / children / styleCaller content/styles; toggle className may be a state callback.
NavigationChipAction is a button, not a link. Use a real named anchor with appropriate chip classes for navigation.
State / recoveryNo automatic data filtering, suggestion model, permission or persistent undo. Consumer owns those outcomes.

State and keyboard

  • Native actions support keyboard activation and explicit outcomes; filter toggles expose pressed state rather than color alone.
  • Do not put a removal button inside another whole-chip button/link. Chip input tokens have one removal action and clear context.
  • The remove button owns Delete/Backspace; it does not capture ordinary editing keys in another input. After removing the focused token, focus an enabled next token, input or independent restore action. The example restores focus to its persistent control.
  • Keep an applied filter and returned result set distinguishable during a real request. Selection does not prove that a server has returned matching data.
  • Represent empty results and clear-filter recovery. Do not claim consent, qualified review or source authority from a color or pressed filter.

Failure cases

  • A div click handler masquerading as an action.
  • Advertised outlined/soft/size props with no actual styling.
  • Several identical Remove names, tiny removal targets, or lost focus after removal.
  • A filter chip that changes only color while the visible result list never changes.
  • A suggestion inventing Indigenous vocabulary or recording provenance.

Verification and release scope

  • Check actual names, roles, state and focus with pointer and keyboard. Inspect the retained value and submitted FormData when relevant.
  • Exercise empty, selected, mixed, disabled, read-only, invalid and pending/recovery states that apply to the control. Styling alone does not prove enforcement.
  • Check both themes, long labels, 320px reflow, meaningful graphic/text contrast, reduced motion, forced colors and the actual target device.
  • Use a named screen-reader/browser pair to verify spoken state. DOM assertions and local examples do not prove announcements or backend persistence.
  • Selection never establishes language quality, source authority, consent, qualified review or permission. Keep that evidence explicit in the product task.