@discourser/design-system 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,945 @@
1
+ # Avatar
2
+
3
+ **Purpose:** Visual representation of users or entities through images, initials, or icons, providing identity context throughout the application.
4
+
5
+ ## Import
6
+
7
+ ```typescript
8
+ import * as Avatar from '@discourser/design-system';
9
+ ```
10
+
11
+ ## Component Structure
12
+
13
+ Avatar uses a compound component pattern with these parts:
14
+
15
+ - `Avatar.Root` - Container for the avatar
16
+ - `Avatar.Image` - Image element for avatar photo
17
+ - `Avatar.Fallback` - Fallback content when image fails or is loading
18
+ - `Avatar.Context` - Context hook for accessing avatar state
19
+ - `Avatar.RootProvider` - Root provider for advanced context usage
20
+
21
+ ## Variants
22
+
23
+ The Avatar component supports 4 visual variants, each with specific use cases:
24
+
25
+ | Variant | Visual Style | Usage | When to Use |
26
+ | --------- | -------------------------------------------- | ------------------------- | --------------------------------------------------- |
27
+ | `subtle` | Light background with colored text | Default avatars | General user representations, comments, posts |
28
+ | `solid` | Solid color background with contrasting text | High emphasis identifiers | Important users, featured profiles, emphasis needed |
29
+ | `surface` | Surface background with border | Outlined avatars | Cards, lists, secondary emphasis |
30
+ | `outline` | Transparent background with border | Minimal emphasis | Subtle user indicators, ghost profiles |
31
+
32
+ ### Visual Characteristics
33
+
34
+ - **subtle**: Uses `colorPalette.subtle.bg` background with `colorPalette.subtle.fg` text (default)
35
+ - **solid**: Uses `colorPalette.solid.bg` background with `colorPalette.solid.fg` text (highest contrast)
36
+ - **surface**: Uses `colorPalette.surface.bg` background with 1px border and `colorPalette.surface.fg` text
37
+ - **outline**: Transparent background with 1px `colorPalette.outline.border` and `colorPalette.outline.fg` text
38
+
39
+ ## Shapes
40
+
41
+ | Shape | Visual Style | Usage | When to Use |
42
+ | --------- | --------------------------- | ----------------- | ------------------------------------------------ |
43
+ | `full` | Fully circular avatar | User profiles | Default, most use cases, follows common patterns |
44
+ | `rounded` | Rounded corners (l3 radius) | Alternative style | Brand consistency, modern UI |
45
+ | `square` | Sharp corners | Non-user entities | Organizations, groups, system accounts |
46
+
47
+ **Recommendation:** Use `full` for individual users. Use `square` for organizations or system entities.
48
+
49
+ ## Sizes
50
+
51
+ | Size | Dimensions | Font Size | Icon Size | Usage |
52
+ | ------ | ---------- | --------- | ---------- | ----------------------------------------------- |
53
+ | `2xs` | 24px (6) | 2xs | 12px (3) | Tiny indicators, inline mentions, compact lists |
54
+ | `xs` | 32px (8) | xs | 16px (4) | Dense layouts, small avatars in groups |
55
+ | `sm` | 36px (9) | sm | 18px (4.5) | Comments, compact user lists, secondary areas |
56
+ | `md` | 40px (10) | md | 20px (5) | Default, most use cases, navigation bars |
57
+ | `lg` | 44px (11) | md | 22px (5.5) | User cards, prominent displays |
58
+ | `xl` | 48px (12) | lg | 24px (6) | Profile headers, featured users |
59
+ | `2xl` | 64px (16) | xl | 32px (8) | Large profile displays, hero sections |
60
+ | `full` | 100% | 100% | Responsive | Container-sized avatars, flexible layouts |
61
+
62
+ **Recommendation:** Use `md` for most cases. Use `sm` or `xs` for compact lists. Use `lg` or larger for profile pages and emphasis.
63
+
64
+ ## Props
65
+
66
+ ### Root Props
67
+
68
+ | Prop | Type | Default | Description |
69
+ | -------------- | ------------------------------------------------------------------ | ---------- | --------------------------------------------------------- |
70
+ | `size` | `'2xs' \| 'xs' \| 'sm' \| 'md' \| 'lg' \| 'xl' \| '2xl' \| 'full'` | `'md'` | Avatar size |
71
+ | `variant` | `'subtle' \| 'solid' \| 'surface' \| 'outline'` | `'subtle'` | Visual style variant |
72
+ | `shape` | `'full' \| 'rounded' \| 'square'` | `'full'` | Avatar shape |
73
+ | `colorPalette` | `string` | - | Color palette for the avatar (e.g., 'primary', 'neutral') |
74
+ | `className` | `string` | - | Additional CSS classes (use sparingly) |
75
+
76
+ ### Image Props
77
+
78
+ | Prop | Type | Default | Description |
79
+ | ---------------- | --------- | --------------- | ------------------------------------------- |
80
+ | `src` | `string` | Required | Image source URL |
81
+ | `alt` | `string` | Required | Alt text for accessibility |
82
+ | `draggable` | `boolean` | `false` | Whether image is draggable (default: false) |
83
+ | `referrerPolicy` | `string` | `'no-referrer'` | Referrer policy for image loading |
84
+
85
+ ### Fallback Props
86
+
87
+ | Prop | Type | Default | Description |
88
+ | ---------- | ----------- | ------- | ------------------------------------------------------- |
89
+ | `name` | `string` | - | Name to derive initials from (e.g., "John Doe" → "JD") |
90
+ | `children` | `ReactNode` | - | Custom fallback content (overrides name-based initials) |
91
+
92
+ **Note:** Avatar components extend their respective HTML element attributes, so all standard attributes are supported.
93
+
94
+ ## Automatic Initials Generation
95
+
96
+ The `Avatar.Fallback` component automatically generates initials from the `name` prop:
97
+
98
+ - **Full name**: "John Doe" → "JD" (first letter of first and last name)
99
+ - **Single name**: "John" → "J" (first letter only)
100
+ - **Multiple names**: "Mary Jane Watson" → "MW" (first and last name only)
101
+ - **No name**: Displays default user icon
102
+
103
+ ## Examples
104
+
105
+ ### Basic Usage
106
+
107
+ ```typescript
108
+ import * as Avatar from '@discourser/design-system';
109
+
110
+ // Avatar with image
111
+ <Avatar.Root colorPalette="primary">
112
+ <Avatar.Fallback name="John Doe" />
113
+ <Avatar.Image src="https://example.com/avatar.jpg" alt="John Doe" />
114
+ </Avatar.Root>
115
+
116
+ // Avatar with initials only
117
+ <Avatar.Root colorPalette="primary">
118
+ <Avatar.Fallback name="Sarah Williams" />
119
+ </Avatar.Root>
120
+
121
+ // Avatar with default icon (no name)
122
+ <Avatar.Root colorPalette="primary">
123
+ <Avatar.Fallback />
124
+ </Avatar.Root>
125
+ ```
126
+
127
+ ### Image Avatars with Fallback
128
+
129
+ ```typescript
130
+ // Image with automatic fallback
131
+ // If image fails to load, shows initials
132
+ <Avatar.Root colorPalette="primary">
133
+ <Avatar.Fallback name="Michael Chen" />
134
+ <Avatar.Image
135
+ src="https://api.example.com/users/123/avatar"
136
+ alt="Michael Chen"
137
+ />
138
+ </Avatar.Root>
139
+
140
+ // Multiple avatars with consistent fallback
141
+ const users = [
142
+ { name: "Alice Johnson", avatar: "https://i.pravatar.cc/150?img=1" },
143
+ { name: "Bob Smith", avatar: "https://i.pravatar.cc/150?img=2" },
144
+ { name: "Carol White", avatar: "https://i.pravatar.cc/150?img=3" },
145
+ ];
146
+
147
+ {users.map(user => (
148
+ <Avatar.Root key={user.name} colorPalette="primary">
149
+ <Avatar.Fallback name={user.name} />
150
+ <Avatar.Image src={user.avatar} alt={user.name} />
151
+ </Avatar.Root>
152
+ ))}
153
+ ```
154
+
155
+ ### Initials-Only Avatars
156
+
157
+ ```typescript
158
+ // Default variant with initials
159
+ <Avatar.Root colorPalette="primary">
160
+ <Avatar.Fallback name="Emily Rodriguez" />
161
+ </Avatar.Root>
162
+
163
+ // Different variants
164
+ <Avatar.Root colorPalette="primary" variant="subtle">
165
+ <Avatar.Fallback name="David Kim" />
166
+ </Avatar.Root>
167
+
168
+ <Avatar.Root colorPalette="primary" variant="solid">
169
+ <Avatar.Fallback name="Lisa Anderson" />
170
+ </Avatar.Root>
171
+
172
+ <Avatar.Root colorPalette="primary" variant="surface">
173
+ <Avatar.Fallback name="Tom Baker" />
174
+ </Avatar.Root>
175
+
176
+ <Avatar.Root colorPalette="primary" variant="outline">
177
+ <Avatar.Fallback name="Nina Patel" />
178
+ </Avatar.Root>
179
+ ```
180
+
181
+ ### Custom Fallback Content
182
+
183
+ ```typescript
184
+ // Custom icon fallback
185
+ <Avatar.Root colorPalette="primary">
186
+ <Avatar.Fallback>
187
+ <CustomUserIcon />
188
+ </Avatar.Fallback>
189
+ </Avatar.Root>
190
+
191
+ // Custom text fallback
192
+ <Avatar.Root colorPalette="primary">
193
+ <Avatar.Fallback>?</Avatar.Fallback>
194
+ </Avatar.Root>
195
+
196
+ // Badge-style fallback
197
+ <Avatar.Root colorPalette="success">
198
+ <Avatar.Fallback>✓</Avatar.Fallback>
199
+ </Avatar.Root>
200
+ ```
201
+
202
+ ### Different Sizes
203
+
204
+ ```typescript
205
+ // Extra small - for inline mentions
206
+ <Avatar.Root colorPalette="primary" size="2xs">
207
+ <Avatar.Fallback name="John Doe" />
208
+ </Avatar.Root>
209
+
210
+ // Small - for comments and compact lists
211
+ <Avatar.Root colorPalette="primary" size="sm">
212
+ <Avatar.Fallback name="Jane Smith" />
213
+ <Avatar.Image src="/avatars/jane.jpg" alt="Jane Smith" />
214
+ </Avatar.Root>
215
+
216
+ // Default - general use
217
+ <Avatar.Root colorPalette="primary" size="md">
218
+ <Avatar.Fallback name="Mike Johnson" />
219
+ <Avatar.Image src="/avatars/mike.jpg" alt="Mike Johnson" />
220
+ </Avatar.Root>
221
+
222
+ // Large - profile displays
223
+ <Avatar.Root colorPalette="primary" size="lg">
224
+ <Avatar.Fallback name="Sarah Williams" />
225
+ <Avatar.Image src="/avatars/sarah.jpg" alt="Sarah Williams" />
226
+ </Avatar.Root>
227
+
228
+ // Extra large - profile headers
229
+ <Avatar.Root colorPalette="primary" size="2xl">
230
+ <Avatar.Fallback name="David Chen" />
231
+ <Avatar.Image src="/avatars/david.jpg" alt="David Chen" />
232
+ </Avatar.Root>
233
+ ```
234
+
235
+ ### Different Shapes
236
+
237
+ ```typescript
238
+ // Circular (default) - for users
239
+ <Avatar.Root colorPalette="primary" shape="full">
240
+ <Avatar.Fallback name="John Doe" />
241
+ </Avatar.Root>
242
+
243
+ // Rounded - modern style
244
+ <Avatar.Root colorPalette="primary" shape="rounded">
245
+ <Avatar.Fallback name="Jane Smith" />
246
+ </Avatar.Root>
247
+
248
+ // Square - for organizations
249
+ <Avatar.Root colorPalette="neutral" shape="square">
250
+ <Avatar.Fallback name="Acme Corp" />
251
+ <Avatar.Image src="/logos/acme.png" alt="Acme Corp" />
252
+ </Avatar.Root>
253
+ ```
254
+
255
+ ### Color Palettes
256
+
257
+ ```typescript
258
+ // Different color palettes for variety
259
+ <Avatar.Root colorPalette="primary">
260
+ <Avatar.Fallback name="User One" />
261
+ </Avatar.Root>
262
+
263
+ <Avatar.Root colorPalette="neutral">
264
+ <Avatar.Fallback name="User Two" />
265
+ </Avatar.Root>
266
+
267
+ <Avatar.Root colorPalette="error">
268
+ <Avatar.Fallback name="User Three" />
269
+ </Avatar.Root>
270
+
271
+ <Avatar.Root colorPalette="success">
272
+ <Avatar.Fallback name="User Four" />
273
+ </Avatar.Root>
274
+
275
+ // Use color palette to represent different user types
276
+ const getColorForRole = (role: string) => {
277
+ const colors = {
278
+ admin: 'error',
279
+ moderator: 'warning',
280
+ member: 'primary',
281
+ guest: 'neutral',
282
+ };
283
+ return colors[role] || 'primary';
284
+ };
285
+
286
+ <Avatar.Root colorPalette={getColorForRole(user.role)}>
287
+ <Avatar.Fallback name={user.name} />
288
+ <Avatar.Image src={user.avatar} alt={user.name} />
289
+ </Avatar.Root>
290
+ ```
291
+
292
+ ## Common Patterns
293
+
294
+ ### Avatar with Status Indicator
295
+
296
+ ```typescript
297
+ import { css } from 'styled-system/css';
298
+
299
+ // Avatar with online status
300
+ <div className={css({ position: 'relative', display: 'inline-flex' })}>
301
+ <Avatar.Root colorPalette="primary" size="md">
302
+ <Avatar.Fallback name="John Doe" />
303
+ <Avatar.Image src="/avatars/john.jpg" alt="John Doe" />
304
+ </Avatar.Root>
305
+
306
+ {/* Status indicator */}
307
+ <div className={css({
308
+ position: 'absolute',
309
+ bottom: '0',
310
+ right: '0',
311
+ width: '3',
312
+ height: '3',
313
+ borderRadius: 'full',
314
+ bg: 'success.solid',
315
+ border: '2px solid',
316
+ borderColor: 'bg.canvas',
317
+ })} />
318
+ </div>
319
+
320
+ // Avatar with availability status
321
+ const StatusAvatar = ({ name, src, status }: Props) => {
322
+ const statusColors = {
323
+ online: 'success.solid',
324
+ away: 'warning.solid',
325
+ busy: 'error.solid',
326
+ offline: 'neutral.subtle',
327
+ };
328
+
329
+ return (
330
+ <div className={css({ position: 'relative', display: 'inline-flex' })}>
331
+ <Avatar.Root colorPalette="primary" size="md">
332
+ <Avatar.Fallback name={name} />
333
+ <Avatar.Image src={src} alt={name} />
334
+ </Avatar.Root>
335
+
336
+ <div className={css({
337
+ position: 'absolute',
338
+ bottom: '0',
339
+ right: '0',
340
+ width: '3',
341
+ height: '3',
342
+ borderRadius: 'full',
343
+ bg: statusColors[status],
344
+ border: '2px solid',
345
+ borderColor: 'bg.canvas',
346
+ })} />
347
+ </div>
348
+ );
349
+ };
350
+ ```
351
+
352
+ ### Avatar Group (Stacked)
353
+
354
+ ```typescript
355
+ import { HStack } from 'styled-system/jsx';
356
+
357
+ // Overlapping avatar group
358
+ <HStack gap="-3">
359
+ <Avatar.Root colorPalette="primary" size="md">
360
+ <Avatar.Fallback name="Alice Johnson" />
361
+ <Avatar.Image src="/avatars/alice.jpg" alt="Alice" />
362
+ </Avatar.Root>
363
+
364
+ <Avatar.Root colorPalette="primary" size="md">
365
+ <Avatar.Fallback name="Bob Smith" />
366
+ <Avatar.Image src="/avatars/bob.jpg" alt="Bob" />
367
+ </Avatar.Root>
368
+
369
+ <Avatar.Root colorPalette="primary" size="md">
370
+ <Avatar.Fallback name="Carol White" />
371
+ <Avatar.Image src="/avatars/carol.jpg" alt="Carol" />
372
+ </Avatar.Root>
373
+
374
+ {/* Count indicator for additional users */}
375
+ <Avatar.Root colorPalette="neutral" size="md">
376
+ <Avatar.Fallback>+5</Avatar.Fallback>
377
+ </Avatar.Root>
378
+ </HStack>
379
+
380
+ // Reusable avatar group component
381
+ const AvatarGroup = ({ users, max = 3, size = 'md' }: Props) => {
382
+ const visibleUsers = users.slice(0, max);
383
+ const remainingCount = users.length - max;
384
+
385
+ return (
386
+ <HStack gap={size === 'sm' ? '-2' : '-3'}>
387
+ {visibleUsers.map(user => (
388
+ <Avatar.Root key={user.id} colorPalette="primary" size={size}>
389
+ <Avatar.Fallback name={user.name} />
390
+ <Avatar.Image src={user.avatar} alt={user.name} />
391
+ </Avatar.Root>
392
+ ))}
393
+
394
+ {remainingCount > 0 && (
395
+ <Avatar.Root colorPalette="neutral" size={size}>
396
+ <Avatar.Fallback>+{remainingCount}</Avatar.Fallback>
397
+ </Avatar.Root>
398
+ )}
399
+ </HStack>
400
+ );
401
+ };
402
+
403
+ <AvatarGroup users={teamMembers} max={4} size="md" />
404
+ ```
405
+
406
+ ### Avatar with Tooltip
407
+
408
+ ```typescript
409
+ import * as Tooltip from '@discourser/design-system';
410
+
411
+ // Avatar with hover tooltip
412
+ <Tooltip.Root>
413
+ <Tooltip.Trigger asChild>
414
+ <Avatar.Root colorPalette="primary" size="md">
415
+ <Avatar.Fallback name="John Doe" />
416
+ <Avatar.Image src="/avatars/john.jpg" alt="John Doe" />
417
+ </Avatar.Root>
418
+ </Tooltip.Trigger>
419
+
420
+ <Tooltip.Positioner>
421
+ <Tooltip.Content>
422
+ <Tooltip.Arrow />
423
+ John Doe - Senior Developer
424
+ </Tooltip.Content>
425
+ </Tooltip.Positioner>
426
+ </Tooltip.Root>
427
+
428
+ // Avatar group with individual tooltips
429
+ {users.map(user => (
430
+ <Tooltip.Root key={user.id}>
431
+ <Tooltip.Trigger asChild>
432
+ <Avatar.Root colorPalette="primary" size="md">
433
+ <Avatar.Fallback name={user.name} />
434
+ <Avatar.Image src={user.avatar} alt={user.name} />
435
+ </Avatar.Root>
436
+ </Tooltip.Trigger>
437
+
438
+ <Tooltip.Positioner>
439
+ <Tooltip.Content>
440
+ <Tooltip.Arrow />
441
+ {user.name} - {user.role}
442
+ </Tooltip.Content>
443
+ </Tooltip.Positioner>
444
+ </Tooltip.Root>
445
+ ))}
446
+ ```
447
+
448
+ ### Avatar in User Card
449
+
450
+ ```typescript
451
+ import { VStack, HStack } from 'styled-system/jsx';
452
+ import { css } from 'styled-system/css';
453
+
454
+ const UserCard = ({ user }: Props) => (
455
+ <div className={css({
456
+ p: '4',
457
+ borderRadius: 'l2',
458
+ bg: 'bg.surface',
459
+ borderWidth: '1px',
460
+ borderColor: 'border.default',
461
+ })}>
462
+ <HStack gap="3" align="center">
463
+ <Avatar.Root colorPalette="primary" size="lg">
464
+ <Avatar.Fallback name={user.name} />
465
+ <Avatar.Image src={user.avatar} alt={user.name} />
466
+ </Avatar.Root>
467
+
468
+ <VStack gap="1" align="start">
469
+ <div className={css({ fontWeight: 'semibold' })}>
470
+ {user.name}
471
+ </div>
472
+ <div className={css({ fontSize: 'sm', color: 'fg.subtle' })}>
473
+ {user.email}
474
+ </div>
475
+ </VStack>
476
+ </HStack>
477
+ </div>
478
+ );
479
+ ```
480
+
481
+ ### Avatar with Upload/Edit
482
+
483
+ ```typescript
484
+ import { css } from 'styled-system/css';
485
+
486
+ // Avatar with edit overlay
487
+ const EditableAvatar = ({ name, src, onEdit }: Props) => (
488
+ <div className={css({ position: 'relative', display: 'inline-flex' })}>
489
+ <Avatar.Root colorPalette="primary" size="2xl">
490
+ <Avatar.Fallback name={name} />
491
+ <Avatar.Image src={src} alt={name} />
492
+ </Avatar.Root>
493
+
494
+ {/* Edit overlay */}
495
+ <button
496
+ onClick={onEdit}
497
+ className={css({
498
+ position: 'absolute',
499
+ inset: '0',
500
+ borderRadius: 'full',
501
+ bg: 'blackAlpha.600',
502
+ color: 'white',
503
+ display: 'flex',
504
+ alignItems: 'center',
505
+ justifyContent: 'center',
506
+ opacity: '0',
507
+ transition: 'opacity 0.2s',
508
+ cursor: 'pointer',
509
+ _hover: { opacity: '1' },
510
+ })}
511
+ >
512
+ <CameraIcon />
513
+ </button>
514
+ </div>
515
+ );
516
+ ```
517
+
518
+ ### Loading State
519
+
520
+ ```typescript
521
+ import { Skeleton } from '@discourser/design-system';
522
+
523
+ // Avatar loading skeleton
524
+ <Skeleton.Root>
525
+ <Skeleton.Circle size="10" />
526
+ </Skeleton.Root>
527
+
528
+ // Conditional loading
529
+ const UserAvatar = ({ user, isLoading }: Props) => {
530
+ if (isLoading) {
531
+ return <Skeleton.Circle size="10" />;
532
+ }
533
+
534
+ return (
535
+ <Avatar.Root colorPalette="primary" size="md">
536
+ <Avatar.Fallback name={user.name} />
537
+ <Avatar.Image src={user.avatar} alt={user.name} />
538
+ </Avatar.Root>
539
+ );
540
+ };
541
+ ```
542
+
543
+ ### Comment/Post Avatar
544
+
545
+ ```typescript
546
+ import { HStack, VStack } from 'styled-system/jsx';
547
+ import { css } from 'styled-system/css';
548
+
549
+ const Comment = ({ comment }: Props) => (
550
+ <HStack gap="3" align="start">
551
+ <Avatar.Root colorPalette="primary" size="sm">
552
+ <Avatar.Fallback name={comment.author.name} />
553
+ <Avatar.Image src={comment.author.avatar} alt={comment.author.name} />
554
+ </Avatar.Root>
555
+
556
+ <VStack gap="1" align="start" flex="1">
557
+ <HStack gap="2" align="center">
558
+ <span className={css({ fontWeight: 'semibold', fontSize: 'sm' })}>
559
+ {comment.author.name}
560
+ </span>
561
+ <span className={css({ fontSize: 'xs', color: 'fg.subtle' })}>
562
+ {comment.timestamp}
563
+ </span>
564
+ </HStack>
565
+
566
+ <p className={css({ fontSize: 'sm' })}>
567
+ {comment.content}
568
+ </p>
569
+ </VStack>
570
+ </HStack>
571
+ );
572
+ ```
573
+
574
+ ### Navigation User Menu
575
+
576
+ ```typescript
577
+ import { HStack } from 'styled-system/jsx';
578
+ import { css } from 'styled-system/css';
579
+
580
+ const UserMenu = ({ user }: Props) => (
581
+ <HStack gap="2" align="center">
582
+ <Avatar.Root colorPalette="primary" size="sm">
583
+ <Avatar.Fallback name={user.name} />
584
+ <Avatar.Image src={user.avatar} alt={user.name} />
585
+ </Avatar.Root>
586
+
587
+ <span className={css({ fontSize: 'sm', fontWeight: 'medium' })}>
588
+ {user.name}
589
+ </span>
590
+
591
+ <ChevronDownIcon />
592
+ </HStack>
593
+ );
594
+ ```
595
+
596
+ ## DO NOT
597
+
598
+ ```typescript
599
+ // ❌ Don't forget the fallback (image might fail to load)
600
+ <Avatar.Root colorPalette="primary">
601
+ <Avatar.Image src={user.avatar} alt={user.name} />
602
+ </Avatar.Root>
603
+
604
+ // ✅ Always provide a fallback
605
+ <Avatar.Root colorPalette="primary">
606
+ <Avatar.Fallback name={user.name} />
607
+ <Avatar.Image src={user.avatar} alt={user.name} />
608
+ </Avatar.Root>
609
+
610
+ // ❌ Don't forget alt text for accessibility
611
+ <Avatar.Image src={user.avatar} />
612
+
613
+ // ✅ Always provide meaningful alt text
614
+ <Avatar.Image src={user.avatar} alt={user.name} />
615
+
616
+ // ❌ Don't use inline styles
617
+ <Avatar.Root style={{ width: '60px' }}>
618
+ <Avatar.Fallback name="User" />
619
+ </Avatar.Root>
620
+
621
+ // ✅ Use size prop
622
+ <Avatar.Root size="xl">
623
+ <Avatar.Fallback name="User" />
624
+ </Avatar.Root>
625
+
626
+ // ❌ Don't use Image without Root
627
+ <Avatar.Image src={user.avatar} alt={user.name} />
628
+
629
+ // ✅ Always wrap with Root
630
+ <Avatar.Root colorPalette="primary">
631
+ <Avatar.Fallback name={user.name} />
632
+ <Avatar.Image src={user.avatar} alt={user.name} />
633
+ </Avatar.Root>
634
+
635
+ // ❌ Don't hardcode initials
636
+ <Avatar.Root colorPalette="primary">
637
+ <Avatar.Fallback>JD</Avatar.Fallback>
638
+ </Avatar.Root>
639
+
640
+ // ✅ Let the component generate initials
641
+ <Avatar.Root colorPalette="primary">
642
+ <Avatar.Fallback name="John Doe" />
643
+ </Avatar.Root>
644
+
645
+ // ❌ Don't use different sizes in avatar groups
646
+ <HStack gap="-3">
647
+ <Avatar.Root size="md"><Avatar.Fallback name="User 1" /></Avatar.Root>
648
+ <Avatar.Root size="lg"><Avatar.Fallback name="User 2" /></Avatar.Root>
649
+ </HStack>
650
+
651
+ // ✅ Keep consistent sizes in groups
652
+ <HStack gap="-3">
653
+ <Avatar.Root size="md"><Avatar.Fallback name="User 1" /></Avatar.Root>
654
+ <Avatar.Root size="md"><Avatar.Fallback name="User 2" /></Avatar.Root>
655
+ </HStack>
656
+
657
+ // ❌ Don't mix shapes in the same context
658
+ <HStack gap="2">
659
+ <Avatar.Root shape="full"><Avatar.Fallback name="User 1" /></Avatar.Root>
660
+ <Avatar.Root shape="square"><Avatar.Fallback name="User 2" /></Avatar.Root>
661
+ </HStack>
662
+
663
+ // ✅ Use consistent shapes in the same context
664
+ <HStack gap="2">
665
+ <Avatar.Root shape="full"><Avatar.Fallback name="User 1" /></Avatar.Root>
666
+ <Avatar.Root shape="full"><Avatar.Fallback name="User 2" /></Avatar.Root>
667
+ </HStack>
668
+ ```
669
+
670
+ ## Accessibility
671
+
672
+ The Avatar component follows WCAG 2.1 Level AA standards:
673
+
674
+ - **Alt Text**: Always provide descriptive alt text for Avatar.Image
675
+ - **Semantic HTML**: Uses proper img elements with alt attributes
676
+ - **Color Contrast**: All variants meet 4.5:1 contrast ratio for text/initials
677
+ - **Non-decorative**: Images convey user identity and should have meaningful alt text
678
+ - **Keyboard Navigation**: If interactive (e.g., clickable), ensure proper keyboard support
679
+
680
+ ### Accessibility Best Practices
681
+
682
+ ```typescript
683
+ // ✅ Provide meaningful alt text
684
+ <Avatar.Root colorPalette="primary">
685
+ <Avatar.Fallback name="Sarah Johnson" />
686
+ <Avatar.Image
687
+ src="/avatars/sarah.jpg"
688
+ alt="Sarah Johnson's profile picture"
689
+ />
690
+ </Avatar.Root>
691
+
692
+ // ✅ For clickable avatars, add proper labels
693
+ <button aria-label="View John Doe's profile">
694
+ <Avatar.Root colorPalette="primary">
695
+ <Avatar.Fallback name="John Doe" />
696
+ <Avatar.Image src="/avatars/john.jpg" alt="John Doe" />
697
+ </Avatar.Root>
698
+ </button>
699
+
700
+ // ✅ For decorative avatars in groups (when text is nearby)
701
+ <HStack gap="2" align="center">
702
+ <Avatar.Root colorPalette="primary" size="sm">
703
+ <Avatar.Fallback name="Jane Smith" />
704
+ <Avatar.Image src="/avatars/jane.jpg" alt="" role="presentation" />
705
+ </Avatar.Root>
706
+ <span>Jane Smith</span>
707
+ </HStack>
708
+
709
+ // ✅ Provide context for avatar groups
710
+ <div role="group" aria-label="Team members">
711
+ <HStack gap="-3">
712
+ {members.map(member => (
713
+ <Avatar.Root key={member.id} colorPalette="primary">
714
+ <Avatar.Fallback name={member.name} />
715
+ <Avatar.Image src={member.avatar} alt={member.name} />
716
+ </Avatar.Root>
717
+ ))}
718
+ </HStack>
719
+ </div>
720
+
721
+ // ✅ Status indicators should be announced
722
+ <div role="img" aria-label="Sarah Johnson, online">
723
+ <Avatar.Root colorPalette="primary">
724
+ <Avatar.Fallback name="Sarah Johnson" />
725
+ <Avatar.Image src="/avatars/sarah.jpg" alt="" />
726
+ </Avatar.Root>
727
+ <span className={css({ srOnly: true })}>Online</span>
728
+ <div className={css({ /* status indicator styles */ })} />
729
+ </div>
730
+ ```
731
+
732
+ ## Size Selection Guide
733
+
734
+ | Scenario | Recommended Size | Reasoning |
735
+ | --------------- | ---------------- | --------------------------------------- |
736
+ | Inline mentions | `2xs` | Minimal disruption to text flow |
737
+ | Comment threads | `sm` | Compact, doesn't overwhelm content |
738
+ | User lists | `sm` or `md` | Balance between recognition and density |
739
+ | Navigation bar | `sm` or `md` | Subtle presence, space-efficient |
740
+ | User cards | `lg` | Prominent, easy to recognize |
741
+ | Profile headers | `xl` or `2xl` | Hero display, maximum recognition |
742
+ | Avatar groups | `sm` or `md` | Better overlap appearance |
743
+ | Sidebar users | `md` | Standard size for side navigation |
744
+ | Chat messages | `sm` | Space-efficient for message threads |
745
+ | Settings pages | `lg` | Clear identity context |
746
+
747
+ ## Variant Selection Guide
748
+
749
+ | Scenario | Recommended Variant | Reasoning |
750
+ | ----------------- | -------------------- | ------------------------------------------------ |
751
+ | Default users | `subtle` | Soft, unobtrusive, works everywhere |
752
+ | Featured users | `solid` | High contrast, draws attention |
753
+ | User cards | `surface` | Defined boundaries, works on colored backgrounds |
754
+ | Minimal UI | `outline` | Lightweight, modern appearance |
755
+ | Dark backgrounds | `solid` or `surface` | Better contrast and visibility |
756
+ | Light backgrounds | `subtle` | Subtle, doesn't compete with content |
757
+
758
+ ## Shape Selection Guide
759
+
760
+ | Scenario | Recommended Shape | Reasoning |
761
+ | ---------------- | --------------------- | ---------------------------------------- |
762
+ | Individual users | `full` | Standard convention, friendly appearance |
763
+ | Organizations | `square` | Distinct from users, formal |
764
+ | Brand logos | `square` or `rounded` | Preserves logo shape and design |
765
+ | Bot accounts | `rounded` | Different from users, still approachable |
766
+ | System users | `square` | Indicates non-human entity |
767
+
768
+ ## State Behaviors
769
+
770
+ | State | Visual Change | Behavior |
771
+ | -------------------------- | ------------------------ | ---------------------------------------------- |
772
+ | **Loading** | Shows fallback | Fallback displays while image loads |
773
+ | **Error** | Shows fallback | Fallback displays if image fails to load |
774
+ | **No Image** | Shows fallback | Displays initials or default icon |
775
+ | **Hover** (if interactive) | Optional visual feedback | Add cursor: pointer and hover styles as needed |
776
+
777
+ ## Responsive Considerations
778
+
779
+ ```typescript
780
+ // Responsive avatar sizes
781
+ import { css } from 'styled-system/css';
782
+
783
+ <Avatar.Root
784
+ colorPalette="primary"
785
+ className={css({
786
+ // Use size tokens for responsive sizing
787
+ size: { base: '8', md: '10', lg: '12' }
788
+ })}
789
+ >
790
+ <Avatar.Fallback name="John Doe" />
791
+ <Avatar.Image src="/avatars/john.jpg" alt="John Doe" />
792
+ </Avatar.Root>
793
+
794
+ // Responsive avatar group
795
+ <HStack
796
+ gap={{ base: '-2', md: '-3' }}
797
+ className={css({
798
+ '& > *': {
799
+ size: { base: 'sm', md: 'md' }
800
+ }
801
+ })}
802
+ >
803
+ {users.map(user => (
804
+ <Avatar.Root key={user.id} colorPalette="primary">
805
+ <Avatar.Fallback name={user.name} />
806
+ <Avatar.Image src={user.avatar} alt={user.name} />
807
+ </Avatar.Root>
808
+ ))}
809
+ </HStack>
810
+
811
+ // Responsive profile header
812
+ <VStack gap={{ base: '3', md: '4' }} align="center">
813
+ <Avatar.Root
814
+ colorPalette="primary"
815
+ size={{ base: 'xl', md: '2xl' }}
816
+ >
817
+ <Avatar.Fallback name={user.name} />
818
+ <Avatar.Image src={user.avatar} alt={user.name} />
819
+ </Avatar.Root>
820
+
821
+ <h1 className={css({
822
+ fontSize: { base: 'xl', md: '2xl' },
823
+ fontWeight: 'bold'
824
+ })}>
825
+ {user.name}
826
+ </h1>
827
+ </VStack>
828
+ ```
829
+
830
+ ## Testing
831
+
832
+ When testing Avatar components:
833
+
834
+ ```typescript
835
+ import { render, screen, waitFor } from '@testing-library/react';
836
+ import * as Avatar from '@discourser/design-system';
837
+
838
+ test('displays image when loaded', async () => {
839
+ render(
840
+ <Avatar.Root colorPalette="primary">
841
+ <Avatar.Fallback name="John Doe" />
842
+ <Avatar.Image src="https://example.com/avatar.jpg" alt="John Doe" />
843
+ </Avatar.Root>
844
+ );
845
+
846
+ const image = await screen.findByAltText('John Doe');
847
+ expect(image).toBeInTheDocument();
848
+ expect(image).toHaveAttribute('src', 'https://example.com/avatar.jpg');
849
+ });
850
+
851
+ test('displays fallback when image fails', async () => {
852
+ render(
853
+ <Avatar.Root colorPalette="primary">
854
+ <Avatar.Fallback name="John Doe" />
855
+ <Avatar.Image src="invalid-url.jpg" alt="John Doe" />
856
+ </Avatar.Root>
857
+ );
858
+
859
+ // Initially shows fallback
860
+ expect(screen.getByText('JD')).toBeInTheDocument();
861
+ });
862
+
863
+ test('generates correct initials from name', () => {
864
+ render(
865
+ <Avatar.Root colorPalette="primary">
866
+ <Avatar.Fallback name="Sarah Jane Williams" />
867
+ </Avatar.Root>
868
+ );
869
+
870
+ // Should show first and last name initials only
871
+ expect(screen.getByText('SW')).toBeInTheDocument();
872
+ });
873
+
874
+ test('displays default icon when no name provided', () => {
875
+ const { container } = render(
876
+ <Avatar.Root colorPalette="primary">
877
+ <Avatar.Fallback />
878
+ </Avatar.Root>
879
+ );
880
+
881
+ // Should render the default user icon SVG
882
+ expect(container.querySelector('svg')).toBeInTheDocument();
883
+ });
884
+
885
+ test('supports custom fallback content', () => {
886
+ render(
887
+ <Avatar.Root colorPalette="primary">
888
+ <Avatar.Fallback>🎉</Avatar.Fallback>
889
+ </Avatar.Root>
890
+ );
891
+
892
+ expect(screen.getByText('🎉')).toBeInTheDocument();
893
+ });
894
+ ```
895
+
896
+ ## Color Palette System
897
+
898
+ Avatar supports the design system's color palette:
899
+
900
+ ```typescript
901
+ // Semantic colors
902
+ <Avatar.Root colorPalette="primary">
903
+ <Avatar.Fallback name="Primary User" />
904
+ </Avatar.Root>
905
+
906
+ <Avatar.Root colorPalette="neutral">
907
+ <Avatar.Fallback name="Neutral User" />
908
+ </Avatar.Root>
909
+
910
+ <Avatar.Root colorPalette="success">
911
+ <Avatar.Fallback name="Success" />
912
+ </Avatar.Root>
913
+
914
+ <Avatar.Root colorPalette="warning">
915
+ <Avatar.Fallback name="Warning" />
916
+ </Avatar.Root>
917
+
918
+ <Avatar.Root colorPalette="error">
919
+ <Avatar.Fallback name="Error" />
920
+ </Avatar.Root>
921
+
922
+ // Use different colors to categorize users
923
+ const getUserColor = (role: string) => {
924
+ const roleColors = {
925
+ admin: 'error',
926
+ moderator: 'warning',
927
+ premium: 'primary',
928
+ free: 'neutral',
929
+ };
930
+ return roleColors[role] || 'neutral';
931
+ };
932
+
933
+ <Avatar.Root colorPalette={getUserColor(user.role)}>
934
+ <Avatar.Fallback name={user.name} />
935
+ <Avatar.Image src={user.avatar} alt={user.name} />
936
+ </Avatar.Root>
937
+ ```
938
+
939
+ ## Related Components
940
+
941
+ - **Badge**: For displaying status or role labels near avatars
942
+ - **Tooltip**: For showing additional user information on hover
943
+ - **Dialog**: For displaying full user profiles
944
+ - **IconButton**: For edit actions on avatars
945
+ - **Skeleton**: For loading states