Skip to main content

Avatar Components

The Avatar Components provide user representation and avatar-related functionality for the WorkPayCore Frontend application. These components handle user avatars, user selection, avatar groups, and labeled avatar displays with consistent styling and behavior.

Overview​

This document covers all avatar-related components that provide user representation, selection, and display functionality with WorkPay's branded styling.

Components Overview​

User Selection​

Avatar Groups​

Avatar Display​


SelectedUserAvatar​

A component for displaying selected users as avatar tags with the ability to remove users from the selection.

Component Location​

import SelectedUserAvatar from 'components/Avatar/SelectedUserAvatar';

Props​

PropTypeRequiredDefaultDescription
usersUser[]✓-Array of selected users
maxUsersToDisplaynumber-5Maximum number of users to display
onRemoveUser(userId: string | number) => void--Callback when a user is removed
classNamestring--Additional CSS class

TypeScript Interface​

interface User {
id: string | number;
name: string;
profile_picture?: string;
}

interface SelectedUserAvatarProps {
users: User[];
maxUsersToDisplay?: number;
onRemoveUser?: (userId: string | number) => void;
className?: string;
}

Features​

User Selection Display​

  • Displays selected users as avatar tags
  • Shows avatar, name, and remove button
  • Configurable maximum display count
  • Overflow indicator for additional users

Interactive Remove Functionality​

  • Close button on each user tag
  • Callback function for user removal
  • Proper event handling for tag removal

Responsive Layout​

  • Flexbox wrap layout for responsive design
  • Consistent spacing and alignment
  • Branded green border styling

Usage Examples​

Basic User Selection​

import SelectedUserAvatar from 'components/Avatar/SelectedUserAvatar';

function UserPicker() {
const [selectedUsers, setSelectedUsers] = useState([
{ id: 1, name: 'John Doe', profile_picture: '/avatars/john.jpg' },
{ id: 2, name: 'Jane Smith', profile_picture: '/avatars/jane.jpg' },
]);

const handleRemoveUser = userId => {
setSelectedUsers(users => users.filter(user => user.id !== userId));
};

return (
<SelectedUserAvatar users={selectedUsers} onRemoveUser={handleRemoveUser} />
);
}

Team Member Assignment​

import SelectedUserAvatar from 'components/Avatar/SelectedUserAvatar';

function TeamAssignment() {
const [teamMembers, setTeamMembers] = useState([]);

const handleRemoveFromTeam = userId => {
setTeamMembers(members => members.filter(member => member.id !== userId));
// Update team assignment in backend
updateTeamAssignment(userId, false);
};

return (
<VStack spacing={4}>
<Heading size='md'>Team Members</Heading>
<SelectedUserAvatar
users={teamMembers}
maxUsersToDisplay={8}
onRemoveUser={handleRemoveFromTeam}
/>
</VStack>
);
}

Custom Display Limit​

import SelectedUserAvatar from 'components/Avatar/SelectedUserAvatar';

function ProjectAssignees() {
const [assignees, setAssignees] = useState([]);

const handleRemoveAssignee = userId => {
setAssignees(current => current.filter(user => user.id !== userId));
};

return (
&lt;Card&gt;
&lt;CardHeader&gt;
<Heading size='sm'>Project Assignees</Heading>
</CardHeader>
&lt;CardBody&gt;
<SelectedUserAvatar
users={assignees}
maxUsersToDisplay={3}
onRemoveUser={handleRemoveAssignee}
/>
</CardBody>
</Card>
);
}

Read-only Display​

import SelectedUserAvatar from 'components/Avatar/SelectedUserAvatar';

function ReadOnlyUserList({ users }) {
return (
<SelectedUserAvatar
users={users}
maxUsersToDisplay={6}
// No onRemoveUser prop makes it read-only
/>
);
}

Styling​

  • Tag Border: 1px solid hue-green.400
  • Tag Padding: 4 units
  • Tag Margin: 2 units
  • Avatar Size: Small (sm)
  • Overflow Indicator: Green background with white text
  • Layout: Flexbox wrap with 2 units spacing

AvatarGroupDropdown​

A dropdown component that displays a group of avatars with a detailed menu showing all users.

Component Location​

import AvatarGroupDropdown from 'components/Avatar/AvatarGroupDropdown';

Props​

PropTypeRequiredDefaultDescription
dataArray<{name: string, src: string}>✓-Array of avatar data
sizestring-'md'Size of avatars (sm, md, lg)

TypeScript Interface​

interface AvatarData {
name: string;
src: string;
}

interface AvatarGroupDropdownProps {
data: AvatarData[];
size?: 'sm' | 'md' | 'lg';
}

Features​

Avatar Group Display​

  • Displays up to 5 avatars in a group
  • Overflow indicator for additional avatars
  • Configurable size (sm, md, lg)
  • Overlapping avatar layout
  • Click to open detailed user list
  • Shows all users with full names
  • Hover effects for menu items
  • Responsive menu positioning

Interactive States​

  • Open/close state management
  • Visual feedback for active state
  • Branded green styling for active state

Usage Examples​

Basic Avatar Group​

import AvatarGroupDropdown from 'components/Avatar/AvatarGroupDropdown';

function TeamMembers() {
const teamData = [
{ name: 'John Doe', src: '/avatars/john.jpg' },
{ name: 'Jane Smith', src: '/avatars/jane.jpg' },
{ name: 'Bob Johnson', src: '/avatars/bob.jpg' },
{ name: 'Alice Brown', src: '/avatars/alice.jpg' },
];

return <AvatarGroupDropdown data={teamData} size='md' />;
}

Project Collaborators​

import AvatarGroupDropdown from 'components/Avatar/AvatarGroupDropdown';

function ProjectCollaborators({ project }) {
const collaborators = project.members.map(member => ({
name: member.full_name,
src: member.profile_picture || '/default-avatar.png',
}));

return (
<HStack spacing={4}>
<Text fontWeight='medium'>Collaborators:</Text>
<AvatarGroupDropdown data={collaborators} size='lg' />
</HStack>
);
}

Different Sizes​

import AvatarGroupDropdown from 'components/Avatar/AvatarGroupDropdown';

function AvatarSizes() {
const sampleData = [
{ name: 'User 1', src: '/avatar1.jpg' },
{ name: 'User 2', src: '/avatar2.jpg' },
{ name: 'User 3', src: '/avatar3.jpg' },
];

return (
<VStack spacing={4}>
<AvatarGroupDropdown data={sampleData} size='sm' />
<AvatarGroupDropdown data={sampleData} size='md' />
<AvatarGroupDropdown data={sampleData} size='lg' />
</VStack>
);
}

Department Team Display​

import AvatarGroupDropdown from 'components/Avatar/AvatarGroupDropdown';

function DepartmentCard({ department }) {
const employeeData = department.employees.map(emp => ({
name: emp.name,
src: emp.profile_picture,
}));

return (
&lt;Card&gt;
&lt;CardHeader&gt;
<Heading size='sm'>{department.name}</Heading>
</CardHeader>
&lt;CardBody&gt;
<HStack justify='space-between'>
&lt;Text&gt;{department.employees.length} employees</Text>
<AvatarGroupDropdown data={employeeData} size='sm' />
</HStack>
</CardBody>
</Card>
);
}

Styling​

AvatarGroup Styling​

  • Max Display: 5 avatars
  • Spacing: Overlapping layout
  • Border: 1px solid white (default), green when open
  • Background: Transparent button, themed on open
  • Z-index: Layered avatars with proper stacking
  • Placement: Bottom-end
  • Menu Items: Hover effects with green background
  • Border: Left border highlight on hover
  • Text: Truncated at 18 characters
  • Padding: No horizontal padding on list

AvatarLabel​

A component that displays an avatar with accompanying text label and description.

Component Location​

import AvatarLabel from 'components/Avatar/AvatarLabel';

Props​

PropTypeRequiredDefaultDescription
namestring✓-Name for avatar fallback
srcstring--Avatar image source
labelstring✓-Primary label text
labelDescstring--Secondary description text
sizestring-'sm'Size of avatar and text (sm, md, lg)

TypeScript Interface​

interface AvatarLabelProps {
name: string;
src?: string;
label: string;
labelDesc?: string;
size?: 'sm' | 'md' | 'lg';
}

Features​

Avatar Display​

  • Configurable avatar size (sm, md, lg)
  • Fallback name initials
  • Profile picture support
  • Consistent sizing with theme

Text Layout​

  • Primary label with medium font weight
  • Secondary description with light font weight
  • Size-responsive typography
  • Proper color schemes (charcoal, slate)

Responsive Design​

  • Wrap layout for proper alignment
  • Flexible column layout for text
  • Consistent spacing and alignment

Usage Examples​

Basic User Card​

import AvatarLabel from 'components/Avatar/AvatarLabel';

function UserCard({ user }) {
return (
<AvatarLabel
name={user.name}
src={user.profile_picture}
label={user.name}
labelDesc={user.email}
size='md'
/>
);
}

Employee Directory​

import AvatarLabel from 'components/Avatar/AvatarLabel';

function EmployeeList({ employees }) {
return (
<VStack spacing={4}>
{employees.map(employee => (
<AvatarLabel
key={employee.id}
name={employee.full_name}
src={employee.profile_picture}
label={employee.full_name}
labelDesc={`${employee.department} • ${employee.position}`}
size='lg'
/>
))}
</VStack>
);
}

Contact Information​

import AvatarLabel from 'components/Avatar/AvatarLabel';

function ContactInfo({ contact }) {
return (
&lt;Card&gt;
&lt;CardBody&gt;
<AvatarLabel
name={contact.name}
src={contact.avatar}
label={contact.name}
labelDesc={contact.phone}
size='md'
/>
</CardBody>
</Card>
);
}

Different Sizes​

import AvatarLabel from 'components/Avatar/AvatarLabel';

function AvatarSizes() {
const user = {
name: 'John Doe',
src: '/avatar.jpg',
label: 'John Doe',
labelDesc: 'Software Engineer',
};

return (
<VStack spacing={6}>
<AvatarLabel {...user} size='sm' />
<AvatarLabel {...user} size='md' />
<AvatarLabel {...user} size='lg' />
</VStack>
);
}

Team Member Profile​

import AvatarLabel from 'components/Avatar/AvatarLabel';

function TeamMemberProfile({ member }) {
return (
<HStack spacing={4} p={4} borderRadius='md' bg='gray.50'>
<AvatarLabel
name={member.name}
src={member.profile_picture}
label={member.name}
labelDesc={`${member.role} • ${member.department}`}
size='lg'
/>
<Spacer />
<Badge colorScheme='green'>{member.status}</Badge>
</HStack>
);
}

Styling​

Avatar Styling​

  • Sizes: sm, md, lg (theme-based)
  • Border: None (default Chakra UI styling)
  • Fallback: Name initials

Text Styling​

  • Label Color: charcoal
  • Label Weight: 500 (medium)
  • Description Color: slate
  • Description Weight: 400 (regular)
  • Font Size: Theme-based size mapping
  • Line Height: Consistent with font size

Layout Styling​

  • Container: Wrap with center alignment
  • Text Container: Flex column
  • Spacing: Consistent wrap spacing

Avatar Patterns​

User Selection Flow​

import { SelectedUserAvatar, AvatarGroupDropdown } from 'components/Avatar';

function UserSelectionExample() {
const [selectedUsers, setSelectedUsers] = useState([]);
const [availableUsers, setAvailableUsers] = useState([]);

const handleUserSelect = user => {
setSelectedUsers(prev => [...prev, user]);
setAvailableUsers(prev => prev.filter(u => u.id !== user.id));
};

const handleUserRemove = userId => {
const removedUser = selectedUsers.find(u => u.id === userId);
setSelectedUsers(prev => prev.filter(u => u.id !== userId));
setAvailableUsers(prev => [...prev, removedUser]);
};

return (
<VStack spacing={4}>
&lt;Box&gt;
<Text mb={2}>Available Users:</Text>
<AvatarGroupDropdown data={availableUsers} size='md' />
</Box>
&lt;Box&gt;
<Text mb={2}>Selected Users:</Text>
<SelectedUserAvatar
users={selectedUsers}
onRemoveUser={handleUserRemove}
/>
</Box>
</VStack>
);
}

Team Display Components​

import { AvatarLabel, AvatarGroupDropdown } from 'components/Avatar';

function TeamDisplay({ team }) {
const teamAvatars = team.members.map(member => ({
name: member.name,
src: member.profile_picture,
}));

return (
&lt;Card&gt;
&lt;CardHeader&gt;
<HStack justify='space-between'>
<AvatarLabel
name={team.leader.name}
src={team.leader.profile_picture}
label={team.leader.name}
labelDesc='Team Leader'
size='md'
/>
<AvatarGroupDropdown data={teamAvatars} size='sm' />
</HStack>
</CardHeader>
</Card>
);
}

Responsive Avatar Display​

import { AvatarLabel, AvatarGroupDropdown } from 'components/Avatar';

function ResponsiveAvatarDisplay({ users, viewMode }) {
if (viewMode === 'detailed') {
return (
<VStack spacing={3}>
{users.map(user => (
<AvatarLabel
key={user.id}
name={user.name}
src={user.profile_picture}
label={user.name}
labelDesc={user.email}
size='md'
/>
))}
</VStack>
);
}

return (
<AvatarGroupDropdown
data={users.map(u => ({ name: u.name, src: u.profile_picture }))}
size='md'
/>
);
}

Best Practices​

User Selection​

  1. Clear Feedback: Provide visual feedback for selected users
  2. Easy Removal: Make it easy to remove selected users
  3. Limit Display: Use reasonable display limits for performance
  4. Consistent Styling: Maintain consistent avatar styling

Avatar Groups​

  1. Performance: Limit the number of avatars for performance
  2. Accessibility: Provide proper labels and descriptions
  3. Responsive Design: Ensure avatars work on different screen sizes
  4. Fallback Images: Always provide fallback for missing images

Text and Labels​

  1. Truncation: Truncate long names appropriately
  2. Hierarchy: Use proper text hierarchy for labels and descriptions
  3. Color Contrast: Ensure sufficient color contrast
  4. Responsive Typography: Use responsive font sizes

Testing​

import { render, screen, fireEvent } from '@testing-library/react';
import SelectedUserAvatar from 'components/Avatar/SelectedUserAvatar';

describe('Avatar Components', () => {
it('should render selected users', () => {
const users = [{ id: 1, name: 'John Doe', profile_picture: '/john.jpg' }];

render(<SelectedUserAvatar users={users} />);
expect(screen.getByText('John Doe')).toBeInTheDocument();
});

it('should call onRemoveUser when close button is clicked', () => {
const onRemoveUser = jest.fn();
const users = [{ id: 1, name: 'John Doe', profile_picture: '/john.jpg' }];

render(<SelectedUserAvatar users={users} onRemoveUser={onRemoveUser} />);

const closeButton = screen.getByLabelText('Close');
fireEvent.click(closeButton);

expect(onRemoveUser).toHaveBeenCalledWith(1);
});

it('should show overflow indicator', () => {
const users = Array(8)
.fill(null)
.map((_, i) => ({
id: i,
name: `User ${i}`,
profile_picture: `/user${i}.jpg`,
}));

render(<SelectedUserAvatar users={users} maxUsersToDisplay={3} />);
expect(screen.getByText('+5')).toBeInTheDocument();
});
});

This comprehensive avatar system provides consistent user representation and selection functionality for the WorkPayCore Frontend application.