An avatar group shows a limited row of people and summarizes everyone who does not fit with a +N badge. Build the React version by deriving the visible slice and hidden count from one user list and one limit. The supplied preview is intentionally incomplete: its fixed values omit one visible collaborator and miscount the overflow.
Implement the derivations inside the App component in App.tsx. Keep the provided users, max, palette, markup, and styles; replace the preview-only visible and overflow values.
max = 4, render AL, AT, GH, and LT, followed by +2.max = 4, render all three avatars and no overflow badge.max = 0, render no avatars and one +6 badge.users.slice(0, max) and users.length - max; no React state is needed.max is a non-negative integer.styles.css.Render the first max users as circles, and turn everyone past that into a single +K badge.
You have a list of people and a fixed amount of room — space for max faces. Show the first max as overlapping avatars, and if the list is longer, replace the tail with one badge that says how many more there are. That is the whole component: a slice and a subtraction, rendered.
Nothing here is stateful. users and max are the inputs; the two things you render — the visible avatars and the overflow count — are derived from them. Slice to get the faces you show; subtract to get the number you hide.
A common first move is to render every user and hope CSS hides the rest:
export default function App() {
return (
<div className="avatars">
{users.map((user, i) => (
<div key={user.name} className="avatar" style={{ background: palette[i] }}>
{initials(user.name)}
</div>
))}
</div>
);
}
It renders all six avatars and never summarizes the two people past the limit. Capping the row visually is not the same as capping the data; you need to slice before you map.
import './styles.css';
const users = [
{ name: 'Ada Lovelace' },
{ name: 'Alan Turing' },
{ name: 'Grace Hopper' },
{ name: 'Linus Torvalds' },
{ name: 'Margaret Hamilton' },
{ name: 'Dennis Ritchie' },
];
const max = 4;
const palette = ['#6366f1', '#0ea5e9', '#10b981', '#f59e0b', '#ef4444', '#8b5cf6'];
function initials(name: string) {
return name.split(' ').map((word) => word[0]).join('').slice(0, 2).toUpperCase();
}
export default function App() {
const visible = users.slice(0, max);
const overflow = users.length - max;
return (
<main className="container">
<h1>Avatar Group</h1>
<div className="avatars" role="list" aria-label="Project collaborators">
{visible.map((user, i) => (
<div
key={user.name}
className="avatar"
role="listitem"
aria-label={user.name}
title={user.name}
style={{ background: palette[i % palette.length] }}
>
{initials(user.name)}
</div>
))}
{overflow > 0 && (
<div className="more" role="listitem" aria-label={`${overflow} more collaborators`}>
+{overflow}
</div>
)}
</div>
</main>
);
}
visible and overflow are plain derived values, recomputed on every render — no useState, because nothing changes over time. Mapping visible (not users) guarantees at most max circles, while the modulo cycles colours if max exceeds the palette length. The overflow > 0 && guard renders the badge only when someone is actually hidden.
The initials are only the visual shorthand. aria-label={user.name} gives each list item the full name, and the overflow item announces the hidden count instead of relying on the symbol alone.
users.length is 6, max is 4.visible = users.slice(0, 4) → Ada, Alan, Grace, Linus → four .avatar circles: AL, AT, GH, LT, coloured palette[0..3].overflow = 6 - 4 = 2, which is > 0, so a .more circle renders showing +2.margin-left: -10px, first child flush left), so the five circles read as one stacked group.users instead of visible — you get every avatar and no summary badge. Slice first.overflow && instead of overflow > 0 && — when overflow is 0, {0 && ...} renders a literal 0 in the row. Compare to > 0.key — fine here since the list is static, but prefer a stable id (the name) so keys survive reordering.AL is ambiguous outside the visual context. Keep the full name in aria-label and title.users and max as props so the same AvatarGroup works anywhere.<img> avatars and keep the initials circle as the fallback when a photo fails to load.+K badge.This version converts the input data into one discriminated render model before mapping it to the same accessible DOM.
import './styles.css';
const users = [
{ name: 'Ada Lovelace' }, { name: 'Alan Turing' }, { name: 'Grace Hopper' },
{ name: 'Linus Torvalds' }, { name: 'Margaret Hamilton' }, { name: 'Dennis Ritchie' },
];
const max = 4;
const palette = ['#6366f1', '#0ea5e9', '#10b981', '#f59e0b', '#ef4444', '#8b5cf6'];
const initials = (name: string) => name.split(' ').map((word) => word[0]).join('').slice(0, 2).toUpperCase();
export default function App() {
const items = [
...users.slice(0, max).map((user, index) => ({ kind: 'user' as const, user, index })),
...(users.length > max ? [{ kind: 'more' as const, count: users.length - max }] : []),
];
return (
<main className="container">
<h1>Avatar Group</h1>
<div className="avatars" role="list" aria-label="Project collaborators">
{items.map((item) => item.kind === 'user' ? (
<div key={item.user.name} className="avatar" role="listitem"
aria-label={item.user.name} title={item.user.name}
style={{ background: palette[item.index % palette.length] }}>
{initials(item.user.name)}
</div>
) : (
<div key="more" className="more" role="listitem" aria-label={`${item.count} more collaborators`}>
+{item.count}
</div>
))}
</div>
</main>
);
}Keep practising the same patterns with a nearby challenge.
No submissions yet
Share your approach and start the discussion.
An avatar group shows a limited row of people and summarizes everyone who does not fit with a +N badge. Build the React version by deriving the visible slice and hidden count from one user list and one limit. The supplied preview is intentionally incomplete: its fixed values omit one visible collaborator and miscount the overflow.
Implement the derivations inside the App component in App.tsx. Keep the provided users, max, palette, markup, and styles; replace the preview-only visible and overflow values.
max = 4, render AL, AT, GH, and LT, followed by +2.max = 4, render all three avatars and no overflow badge.max = 0, render no avatars and one +6 badge.users.slice(0, max) and users.length - max; no React state is needed.max is a non-negative integer.styles.css.Render the first max users as circles, and turn everyone past that into a single +K badge.
You have a list of people and a fixed amount of room — space for max faces. Show the first max as overlapping avatars, and if the list is longer, replace the tail with one badge that says how many more there are. That is the whole component: a slice and a subtraction, rendered.
Nothing here is stateful. users and max are the inputs; the two things you render — the visible avatars and the overflow count — are derived from them. Slice to get the faces you show; subtract to get the number you hide.
A common first move is to render every user and hope CSS hides the rest:
export default function App() {
return (
<div className="avatars">
{users.map((user, i) => (
<div key={user.name} className="avatar" style={{ background: palette[i] }}>
{initials(user.name)}
</div>
))}
</div>
);
}
It renders all six avatars and never summarizes the two people past the limit. Capping the row visually is not the same as capping the data; you need to slice before you map.
import './styles.css';
const users = [
{ name: 'Ada Lovelace' },
{ name: 'Alan Turing' },
{ name: 'Grace Hopper' },
{ name: 'Linus Torvalds' },
{ name: 'Margaret Hamilton' },
{ name: 'Dennis Ritchie' },
];
const max = 4;
const palette = ['#6366f1', '#0ea5e9', '#10b981', '#f59e0b', '#ef4444', '#8b5cf6'];
function initials(name: string) {
return name.split(' ').map((word) => word[0]).join('').slice(0, 2).toUpperCase();
}
export default function App() {
const visible = users.slice(0, max);
const overflow = users.length - max;
return (
<main className="container">
<h1>Avatar Group</h1>
<div className="avatars" role="list" aria-label="Project collaborators">
{visible.map((user, i) => (
<div
key={user.name}
className="avatar"
role="listitem"
aria-label={user.name}
title={user.name}
style={{ background: palette[i % palette.length] }}
>
{initials(user.name)}
</div>
))}
{overflow > 0 && (
<div className="more" role="listitem" aria-label={`${overflow} more collaborators`}>
+{overflow}
</div>
)}
</div>
</main>
);
}
visible and overflow are plain derived values, recomputed on every render — no useState, because nothing changes over time. Mapping visible (not users) guarantees at most max circles, while the modulo cycles colours if max exceeds the palette length. The overflow > 0 && guard renders the badge only when someone is actually hidden.
The initials are only the visual shorthand. aria-label={user.name} gives each list item the full name, and the overflow item announces the hidden count instead of relying on the symbol alone.
users.length is 6, max is 4.visible = users.slice(0, 4) → Ada, Alan, Grace, Linus → four .avatar circles: AL, AT, GH, LT, coloured palette[0..3].overflow = 6 - 4 = 2, which is > 0, so a .more circle renders showing +2.margin-left: -10px, first child flush left), so the five circles read as one stacked group.users instead of visible — you get every avatar and no summary badge. Slice first.overflow && instead of overflow > 0 && — when overflow is 0, {0 && ...} renders a literal 0 in the row. Compare to > 0.key — fine here since the list is static, but prefer a stable id (the name) so keys survive reordering.AL is ambiguous outside the visual context. Keep the full name in aria-label and title.users and max as props so the same AvatarGroup works anywhere.<img> avatars and keep the initials circle as the fallback when a photo fails to load.+K badge.This version converts the input data into one discriminated render model before mapping it to the same accessible DOM.
import './styles.css';
const users = [
{ name: 'Ada Lovelace' }, { name: 'Alan Turing' }, { name: 'Grace Hopper' },
{ name: 'Linus Torvalds' }, { name: 'Margaret Hamilton' }, { name: 'Dennis Ritchie' },
];
const max = 4;
const palette = ['#6366f1', '#0ea5e9', '#10b981', '#f59e0b', '#ef4444', '#8b5cf6'];
const initials = (name: string) => name.split(' ').map((word) => word[0]).join('').slice(0, 2).toUpperCase();
export default function App() {
const items = [
...users.slice(0, max).map((user, index) => ({ kind: 'user' as const, user, index })),
...(users.length > max ? [{ kind: 'more' as const, count: users.length - max }] : []),
];
return (
<main className="container">
<h1>Avatar Group</h1>
<div className="avatars" role="list" aria-label="Project collaborators">
{items.map((item) => item.kind === 'user' ? (
<div key={item.user.name} className="avatar" role="listitem"
aria-label={item.user.name} title={item.user.name}
style={{ background: palette[item.index % palette.length] }}>
{initials(item.user.name)}
</div>
) : (
<div key="more" className="more" role="listitem" aria-label={`${item.count} more collaborators`}>
+{item.count}
</div>
))}
</div>
</main>
);
}Keep practising the same patterns with a nearby challenge.
No submissions yet
Share your approach and start the discussion.