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.