diff --git a/components/ui/__tests__/avatar-initials-contrast.test.tsx b/components/ui/__tests__/avatar-initials-contrast.test.tsx
new file mode 100644
index 000000000..c40bfa20b
--- /dev/null
+++ b/components/ui/__tests__/avatar-initials-contrast.test.tsx
@@ -0,0 +1,22 @@
+import { render } from '@testing-library/react';
+import { describe, expect, it } from 'vitest';
+import { Avatar } from '../avatar';
+import { getLuminance, parseColor } from '@/lib/color-transform';
+
+// LV-10: white initials failed 4.5:1 on the light generated colours.
+
+function ratio(fg: string, bg: string): number {
+ const [la, lb] = [parseColor(fg)!, parseColor(bg)!].map((c) => getLuminance(c.r, c.g, c.b));
+ return (Math.max(la, lb) + 0.05) / (Math.min(la, lb) + 0.05);
+}
+
+describe('avatar initials', () => {
+ it('read at 4.5:1 on any generated colour', () => {
+ for (const name of ['Alice Smith', 'Bob Chen', 'Carol Lee', 'Dev User', 'Erik Ström', 'Fatima Noor', 'Gao Wei', 'Hana Kim']) {
+ const { container, unmount } = render();
+ const el = container.firstElementChild as HTMLElement;
+ expect(ratio(el.style.color, el.style.backgroundColor)).toBeGreaterThanOrEqual(4.5);
+ unmount();
+ }
+ });
+});
diff --git a/components/ui/avatar.tsx b/components/ui/avatar.tsx
index 96db820ae..cecbaab8e 100644
--- a/components/ui/avatar.tsx
+++ b/components/ui/avatar.tsx
@@ -2,6 +2,7 @@
import { useState, useCallback, useMemo, useEffect } from "react";
import { cn } from "@/lib/utils";
+import { getLuminance, parseColor } from "@/lib/color-transform";
import { useSettingsStore } from "@/stores/settings-store";
import { useContactStore, getContactPhotoIndex, type ContactPhotoRef } from "@/stores/contact-store";
import { useConfig } from "@/hooks/use-config";
@@ -156,6 +157,18 @@ interface AvatarProps {
fallbackColor?: string;
}
+/**
+ * White or black initials, whichever contrasts more with the background.
+ * One of the two always reaches 4.58:1, so the initials pass 4.5:1 on every
+ * generated colour (white alone failed on yellows and greens).
+ */
+function initialsColor(background: string): string {
+ const rgb = parseColor(background);
+ if (!rgb) return '#ffffff';
+ const l = getLuminance(rgb.r, rgb.g, rgb.b);
+ return 1.05 / (l + 0.05) >= (l + 0.05) / 0.05 ? '#ffffff' : '#000000';
+}
+
export function Avatar({ name, email, contactPhoto, size = "md", className, disableImages = false, disableFavicon = false, fallbackColor }: AvatarProps) {
const [imgError, setImgError] = useState(false);
const [pluginAvatarUrl, setPluginAvatarUrl] = useState(null);
@@ -272,6 +285,8 @@ export function Avatar({ name, email, contactPhoto, size = "md", className, disa
}
}, [imgSrc, pluginAvatar, faviconDomain, resolvedContactPhoto, customAvatar, profilePic]);
+ const initialsBackground = fallbackColor ?? getBackgroundColor();
+
return (
{imgSrc ? (