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
- SelectedUserAvatar - User selection component with avatar tags and remove functionality
Avatar Groups
- AvatarGroupDropdown - Dropdown menu showing avatar group with detailed list
Avatar Display
- AvatarLabel - Avatar with text label and description
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
| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
| users | User[] | ✓ | - | Array of selected users |
| maxUsersToDisplay | number | - | 5 | Maximum number of users to display |
| onRemoveUser | (userId: string | number) => void | - | - | Callback when a user is removed |
| className | string | - | - | 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 (
<Card>
<CardHeader>
<Heading size='sm'>Project Assignees</Heading>
</CardHeader>
<CardBody>
<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
| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
| data | Array<{name: string, src: string}> | ✓ | - | Array of avatar data |
| size | string | - | '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
Dropdown Menu
- 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 (
<Card>
<CardHeader>
<Heading size='sm'>{department.name}</Heading>
</CardHeader>
<CardBody>
<HStack justify='space-between'>
<Text>{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
Menu Styling
- 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
| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
| name | string | ✓ | - | Name for avatar fallback |
| src | string | - | - | Avatar image source |
| label | string | ✓ | - | Primary label text |
| labelDesc | string | - | - | Secondary description text |
| size | string | - | '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 (
<Card>
<CardBody>
<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}>
<Box>
<Text mb={2}>Available Users:</Text>
<AvatarGroupDropdown data={availableUsers} size='md' />
</Box>
<Box>
<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 (
<Card>
<CardHeader>
<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
- Clear Feedback: Provide visual feedback for selected users
- Easy Removal: Make it easy to remove selected users
- Limit Display: Use reasonable display limits for performance
- Consistent Styling: Maintain consistent avatar styling
Avatar Groups
- Performance: Limit the number of avatars for performance
- Accessibility: Provide proper labels and descriptions
- Responsive Design: Ensure avatars work on different screen sizes
- Fallback Images: Always provide fallback for missing images
Text and Labels
- Truncation: Truncate long names appropriately
- Hierarchy: Use proper text hierarchy for labels and descriptions
- Color Contrast: Ensure sufficient color contrast
- 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.