Summary
Make the search experience faster, more accessible, and more polished. The current SearchBox is functional but has several UX and a11y gaps: no associated label, vague placeholder, no way to clear the query without manually deleting text, and every keystroke triggers an immediate filter with no debouncing.
Current Behavior
- No label / poor a11y:
src/components/SearchBox.js:6-11 renders a bare <input type=\"search\" placeholder=\"robot\"> with no <label>, no aria-label, and no aria-describedby. Screen readers announce it poorly. The placeholder \"robot\" is also vague (singular, no hint that it filters by name).
- Uncontrolled input: The input is uncontrolled —
App.js:38 passes onSearchChange but never passes value={searchfield}. Clearing the field programmatically (e.g. a "Clear" button) would not reset the visible input without this.
- No debouncing:
src/containers/App.js:24-26 calls setState on every keystroke and render() re-filters the full list immediately. Fine for 10 users from jsonplaceholder, but the pattern does not scale and causes unnecessary renders.
- No clear affordance: Once a user types a query there is no
× / "Clear" button. They must select-all + delete. On mobile this is especially tedious.
- Scroll container is rigid:
src/components/Scroll.js:5 hardcodes height: '470px' with an inline style and a harsh 1px solid black border. It does not adapt to viewport height and looks cramped on small screens.
- Card image alt is generic:
src/components/Card.js:6 uses alt=\"photo\" — not descriptive for assistive tech.
Steps to reproduce
npm start → open http://localhost:3000.
- Inspect the search input with DevTools → no
<label> associated.
- Run Lighthouse Accessibility audit → flags "Form elements do not have associated labels".
- Type quickly in the search box → every keystroke triggers a render (visible in React DevTools Profiler).
- Type
Leanne → no way to clear except manually deleting; shrink viewport to 375px wide → scroll area overflows / feels cramped.
Expected Behavior
- Accessible search input — Input has an associated label (visually hidden if needed) and proper ARIA attributes. Placeholder is descriptive. Lighthouse a11y for the control passes.
- Controlled input — Input value is driven by
App.state.searchfield.
- Debounced filtering — Filtering is debounced (~250–300ms) so fast typing does not thrash renders. Immediate feedback still feels snappy.
- Clear button — An
× button appears when searchfield is non-empty; clicking it clears the query, refocuses the input, and restores the full list.
- Responsive scroll area — Scroll container adapts to viewport (e.g.
max-height: 60vh or flex layout) instead of a fixed 470px, with a softer border consistent with Tachyons styling.
- Descriptive alt text — Card images use
alt=\"Robot avatar for {name}\" or similar.
Proposal
1. Make SearchBox controlled + accessible (src/components/SearchBox.js)
const SearchBox = ({ value, searchChange, onClear }) => {
return (
<div className=\"pa2 relative\">
<label htmlFor=\"search-robots\" className=\"clip\">Search robots</label>
<input
id=\"search-robots\"
className=\"pa3 ba b--green bg-lightest-blue\"
type=\"search\"
placeholder=\"Search robots by name\"
aria-label=\"Search robots by name\"
value={value}
onChange={searchChange}
/>
{value && (
<button
aria-label=\"Clear search\"
className=\"ml2 pa2 ba b--green bg-white pointer\"
onClick={onClear}
>
×
</button>
)}
</div>
);
};
clip is a Tachyons helper for visually-hidden but screen-reader-visible text (already available via tachyons).
- Keep styling consistent with existing Tachyons classes.
2. Wire up controlled value + clear + debounce in src/containers/App.js
// state
this.state = { robots: [], searchfield: '', /* existing */ }
// make input controlled
<SearchBox
value={this.state.searchfield}
searchChange={this.onSearchChange}
onClear={() => this.setState({ searchfield: '' })}
/>
Debounce option A (simple, no new dep):
Add a debounced copy of the field:
// in constructor
this.state = { robots: [], searchfield: '', debouncedSearch: '' };
this.debouncedSetSearch = debounce((val) => this.setState({ debouncedSearch: val }), 300);
// onSearchChange
onSearchChange = (e) => {
const val = e.target.value;
this.setState({ searchfield: val });
this.debouncedSetSearch(val);
};
// filter against debouncedSearch, not searchfield
Provide a tiny debounce util inline (no new dependency) or install lodash.debounce — either is acceptable, document the choice.
Debounce option B: Use useDeferredValue / useTransition — only if App is first migrated to a function component (out of scope for this ticket, but call it out if the assignee prefers).
3. Responsive Scroll (src/components/Scroll.js)
<div style={{ overflowY: 'auto', border: '1px solid #e0e0e0', maxHeight: '60vh' }}>
{props.children}
</div>
Or move to a CSS class in src/containers/app.css. Avoid inline height: 470px.
4. Card alt text (src/components/Card.js:6)
<img alt={`Robot avatar for ${name}`} src={`https://robohash.org/${id}?50x50`} />
Note: also fix the robohash URL param — current is ?50*50 (typo), should be ?size=200x200 or ?50x50.
Acceptance Criteria
Files to touch
src/components/SearchBox.js — main change
src/containers/App.js — wire value/onClear/debounce
src/components/Scroll.js — responsive height/border
src/components/Card.js — alt text + robohash URL fix (one-liner)
Out of Scope
Labels
enhancement, good first issue, a11y, UX
Priority
Medium
Effort
Small (~2–3 hours)
Summary
Make the search experience faster, more accessible, and more polished. The current
SearchBoxis functional but has several UX and a11y gaps: no associated label, vague placeholder, no way to clear the query without manually deleting text, and every keystroke triggers an immediate filter with no debouncing.Current Behavior
src/components/SearchBox.js:6-11renders a bare<input type=\"search\" placeholder=\"robot\">with no<label>, noaria-label, and noaria-describedby. Screen readers announce it poorly. The placeholder\"robot\"is also vague (singular, no hint that it filters by name).App.js:38passesonSearchChangebut never passesvalue={searchfield}. Clearing the field programmatically (e.g. a "Clear" button) would not reset the visible input without this.src/containers/App.js:24-26callssetStateon every keystroke andrender()re-filters the full list immediately. Fine for 10 users fromjsonplaceholder, but the pattern does not scale and causes unnecessary renders.×/ "Clear" button. They must select-all + delete. On mobile this is especially tedious.src/components/Scroll.js:5hardcodesheight: '470px'with an inline style and a harsh1px solid blackborder. It does not adapt to viewport height and looks cramped on small screens.src/components/Card.js:6usesalt=\"photo\"— not descriptive for assistive tech.Steps to reproduce
npm start→ openhttp://localhost:3000.<label>associated.Leanne→ no way to clear except manually deleting; shrink viewport to 375px wide → scroll area overflows / feels cramped.Expected Behavior
App.state.searchfield.×button appears whensearchfieldis non-empty; clicking it clears the query, refocuses the input, and restores the full list.max-height: 60vhor flex layout) instead of a fixed 470px, with a softer border consistent with Tachyons styling.alt=\"Robot avatar for {name}\"or similar.Proposal
1. Make SearchBox controlled + accessible (
src/components/SearchBox.js)clipis a Tachyons helper for visually-hidden but screen-reader-visible text (already available viatachyons).2. Wire up controlled value + clear + debounce in
src/containers/App.jsDebounce option A (simple, no new dep):
Add a debounced copy of the field:
Provide a tiny
debounceutil inline (no new dependency) or installlodash.debounce— either is acceptable, document the choice.Debounce option B: Use
useDeferredValue/useTransition— only if App is first migrated to a function component (out of scope for this ticket, but call it out if the assignee prefers).3. Responsive Scroll (
src/components/Scroll.js)Or move to a CSS class in
src/containers/app.css. Avoid inlineheight: 470px.4. Card alt text (
src/components/Card.js:6)Note: also fix the robohash URL param — current is
?50*50(typo), should be?size=200x200or?50x50.Acceptance Criteria
<label>(oraria-label) and Lighthouse "Form elements do not have associated labels" no longer flags it.value={searchfield}— typing and programmatic clear both update the visible field.×) button is visible only whensearchfieldis non-empty; clicking it clears the field, refocuses the input, and restores all robots.Scrollcontainer is responsive (no fixed470pxheight) and border is softened to match the Tachyons palette.alttext is descriptive and includes the robot name.toLowerCase()inApp.js:29-31) still works.Files to touch
src/components/SearchBox.js— main changesrc/containers/App.js— wirevalue/onClear/debouncesrc/components/Scroll.js— responsive height/bordersrc/components/Card.js— alt text + robohash URL fix (one-liner)Out of Scope
ErrorBoundry/ error + empty states (covered by Handle API errors and show empty state when search has no results #2)Appfrom class component to hooks/Redux (separate ticket)Labels
enhancement,good first issue,a11y,UXPriority
Medium
Effort
Small (~2–3 hours)