+
+ Check Color Contrast for WCAG Compliance
+
+ Ensure your designs meet accessibility standards with our free WCAG
+ color contrast checker. This tool helps you verify that your color
+ combinations meet WCAG 2.1 AA and AAA compliance requirements for both
+ normal and large text.
+
+
+
+
+ How to Use Jam's WCAG Color Contrast Checker
+
+ Quickly verify color contrast ratios and ensure your designs meet
+ accessibility standards. Perfect for designers, developers, and anyone
+ working on accessible web content.
+
+
+ -
+ Enter your foreground color: Input your text color in hex
+ format (e.g., #000000)
+
+ -
+ Enter your background color: Input your background color in
+ hex format (e.g., #FFFFFF)
+
+ -
+ Review the results: Instantly see the contrast ratio and WCAG
+ compliance status for both AA and AAA standards
+
+
+
+
+
+ Understanding WCAG Color Contrast Requirements
+
+ The Web Content Accessibility Guidelines (WCAG) 2.1 define minimum
+ contrast ratios to ensure text is readable for people with visual
+ impairments. The contrast ratio measures the difference in luminance
+ between text and background colors.
+
+
+ -
+ WCAG AA Normal text: Requires 4.5:1 contrast ratio (text
+ smaller than 18pt or 14pt bold)
+
+ -
+ WCAG AA Large text: Requires 3:1 contrast ratio (text 18pt+
+ or 14pt+ bold)
+
+ -
+ WCAG AA Graphical objects and UI components: Requires 3:1
+ contrast ratio for essential visual information like icons, buttons,
+ form controls, and other interactive elements
+
+ -
+ WCAG AAA Normal text: Requires 7:1 contrast ratio (text
+ smaller than 18pt or 14pt bold)
+
+ -
+ WCAG AAA Large text: Requires 4.5:1 contrast ratio (text
+ 18pt+ or 14pt+ bold)
+
+
+
+
+
+ Graphical Objects and User Interface Components
+
+ WCAG 2.1 also requires a minimum 3:1 contrast ratio for graphical
+ objects and user interface components. This includes icons, buttons,
+ form controls, charts, graphs, and other essential visual elements
+ that convey information or require user interaction. Our tool checks
+ compliance for these elements, helping you ensure that all parts of
+ your interface are accessible to users with visual impairments.
+
+
+
+
+ Why Color Contrast Matters for Accessibility
+
+ Proper color contrast is essential for accessibility. It ensures that
+ people with visual impairments, color blindness, or those viewing
+ content in bright sunlight can read your text and interact with your
+ interface. Meeting WCAG standards not only improves accessibility but
+ also helps you comply with legal requirements in many jurisdictions.
+ Our tool calculates the contrast ratio using the WCAG 2.1 formula,
+ which considers the relative luminance of colors.
+
+
+
+ );
+}
diff --git a/components/utils/tools-list.ts b/components/utils/tools-list.ts
index e4c3ada..a5ac9ba 100644
--- a/components/utils/tools-list.ts
+++ b/components/utils/tools-list.ts
@@ -161,4 +161,10 @@ export const tools = [
"Generate cryptographically secure random strings with configurable character sets. Perfect for API keys, tokens, passwords, and secure identifiers.",
link: "/utilities/random-string-generator",
},
+ {
+ title: "WCAG Color Contrast Checker",
+ description:
+ "Check color contrast ratios for WCAG AA and AAA compliance. Ensure your designs meet accessibility standards with our free color contrast checker tool.",
+ link: "/utilities/wcag-color-contrast-checker",
+ },
];
diff --git a/components/utils/wcag-color-contrast.utils.test.ts b/components/utils/wcag-color-contrast.utils.test.ts
new file mode 100644
index 0000000..a2a0c98
--- /dev/null
+++ b/components/utils/wcag-color-contrast.utils.test.ts
@@ -0,0 +1,484 @@
+import {
+ hexToRgb,
+ isValidHex,
+ rgbToHex,
+ normalizeHexForDisplay,
+ normalizeHexInput,
+ getRelativeLuminance,
+ getContrastRatio,
+ checkWCAGCompliance,
+ calculateContrast,
+ getContrastDescription,
+ RGB,
+} from "./wcag-color-contrast.utils";
+
+describe("wcag-color-contrast.utils", () => {
+ describe("hexToRgb", () => {
+ test("should convert valid hex color to RGB", () => {
+ expect(hexToRgb("#000000")).toEqual({ r: 0, g: 0, b: 0 });
+ expect(hexToRgb("#FFFFFF")).toEqual({ r: 255, g: 255, b: 255 });
+ expect(hexToRgb("#FF0000")).toEqual({ r: 255, g: 0, b: 0 });
+ expect(hexToRgb("#00FF00")).toEqual({ r: 0, g: 255, b: 0 });
+ expect(hexToRgb("#0000FF")).toEqual({ r: 0, g: 0, b: 255 });
+ expect(hexToRgb("#767676")).toEqual({ r: 118, g: 118, b: 118 });
+ });
+
+ test("should convert 3-digit hex color to RGB (expanded)", () => {
+ expect(hexToRgb("#000")).toEqual({ r: 0, g: 0, b: 0 });
+ expect(hexToRgb("#fff")).toEqual({ r: 255, g: 255, b: 255 });
+ expect(hexToRgb("#FFF")).toEqual({ r: 255, g: 255, b: 255 });
+ expect(hexToRgb("#f00")).toEqual({ r: 255, g: 0, b: 0 });
+ expect(hexToRgb("#0f0")).toEqual({ r: 0, g: 255, b: 0 });
+ expect(hexToRgb("#00f")).toEqual({ r: 0, g: 0, b: 255 });
+ expect(hexToRgb("#abc")).toEqual({ r: 170, g: 187, b: 204 });
+ });
+
+ test("should convert hex without # prefix", () => {
+ expect(hexToRgb("000000")).toEqual({ r: 0, g: 0, b: 0 });
+ expect(hexToRgb("FFFFFF")).toEqual({ r: 255, g: 255, b: 255 });
+ expect(hexToRgb("fff")).toEqual({ r: 255, g: 255, b: 255 });
+ expect(hexToRgb("000")).toEqual({ r: 0, g: 0, b: 0 });
+ });
+
+ test("should return null for invalid hex colors", () => {
+ expect(hexToRgb("#GGGGGG")).toBeNull();
+ expect(hexToRgb("invalid")).toBeNull();
+ expect(hexToRgb("#12")).toBeNull();
+ expect(hexToRgb("#1234567")).toBeNull();
+ expect(hexToRgb("#123456789")).toBeNull();
+ });
+
+ test("should handle 4 and 5 digit hex colors (treated as 3 during typing)", () => {
+ const result4 = hexToRgb("#fff0");
+ expect(result4).not.toBeNull();
+ expect(result4).toEqual({ r: 255, g: 255, b: 255 });
+
+ const result5 = hexToRgb("#12345");
+ expect(result5).not.toBeNull();
+ });
+
+ test("should return null for invalid lengths", () => {
+ expect(hexToRgb("#12")).toBeNull();
+ expect(hexToRgb("#1234567")).toBeNull();
+ expect(hexToRgb("#12345678")).toBeNull();
+ });
+ });
+
+ describe("isValidHex", () => {
+ test("should validate correct hex colors", () => {
+ expect(isValidHex("#000000")).toBe(true);
+ expect(isValidHex("#FFFFFF")).toBe(true);
+ expect(isValidHex("#ffffff")).toBe(true);
+ expect(isValidHex("#FF5733")).toBe(true);
+ expect(isValidHex("000000")).toBe(true);
+ expect(isValidHex("FFFFFF")).toBe(true);
+ });
+
+ test("should validate 3-digit hex colors", () => {
+ expect(isValidHex("#000")).toBe(true);
+ expect(isValidHex("#fff")).toBe(true);
+ expect(isValidHex("#FFF")).toBe(true);
+ expect(isValidHex("#abc")).toBe(true);
+ expect(isValidHex("000")).toBe(true);
+ expect(isValidHex("fff")).toBe(true);
+ expect(isValidHex("ABC")).toBe(true);
+ });
+
+ test("should validate 4 and 5 digit hex colors (accepted during typing)", () => {
+ expect(isValidHex("#fff0")).toBe(true);
+ expect(isValidHex("#12345")).toBe(true);
+ });
+
+ test("should reject invalid hex colors", () => {
+ expect(isValidHex("#GGGGGG")).toBe(false);
+ expect(isValidHex("invalid")).toBe(false);
+ expect(isValidHex("#12")).toBe(false);
+ expect(isValidHex("#1234567")).toBe(false);
+ expect(isValidHex("#123456789")).toBe(false);
+ expect(isValidHex("")).toBe(false);
+ });
+ });
+
+ describe("rgbToHex", () => {
+ test("should convert RGB to hex", () => {
+ expect(rgbToHex({ r: 0, g: 0, b: 0 })).toBe("#000000");
+ expect(rgbToHex({ r: 255, g: 255, b: 255 })).toBe("#FFFFFF");
+ expect(rgbToHex({ r: 255, g: 0, b: 0 })).toBe("#FF0000");
+ expect(rgbToHex({ r: 0, g: 255, b: 0 })).toBe("#00FF00");
+ expect(rgbToHex({ r: 0, g: 0, b: 255 })).toBe("#0000FF");
+ expect(rgbToHex({ r: 118, g: 118, b: 118 })).toBe("#767676");
+ });
+
+ test("should pad single digit values with zeros", () => {
+ expect(rgbToHex({ r: 10, g: 5, b: 1 })).toBe("#0A0501");
+ expect(rgbToHex({ r: 0, g: 15, b: 255 })).toBe("#000FFF");
+ });
+
+ test("should convert to uppercase", () => {
+ expect(rgbToHex({ r: 170, g: 187, b: 204 })).toBe("#AABBCC");
+ });
+ });
+
+ describe("normalizeHexForDisplay", () => {
+ test("should normalize 3-digit hex to 6-digit", () => {
+ expect(normalizeHexForDisplay("#000")).toBe("#000000");
+ expect(normalizeHexForDisplay("#fff")).toBe("#FFFFFF");
+ expect(normalizeHexForDisplay("#FFF")).toBe("#FFFFFF");
+ expect(normalizeHexForDisplay("#abc")).toBe("#AABBCC");
+ expect(normalizeHexForDisplay("#f00")).toBe("#FF0000");
+ });
+
+ test("should normalize 4 and 5 digit hex to 6-digit (using first 3)", () => {
+ expect(normalizeHexForDisplay("#fff0")).toBe("#FFFFFF");
+ expect(normalizeHexForDisplay("#12345")).toBe("#112233");
+ });
+
+ test("should return 6-digit hex as is (uppercase)", () => {
+ expect(normalizeHexForDisplay("#000000")).toBe("#000000");
+ expect(normalizeHexForDisplay("#ffffff")).toBe("#FFFFFF");
+ expect(normalizeHexForDisplay("#FFFFFF")).toBe("#FFFFFF");
+ expect(normalizeHexForDisplay("#FF5733")).toBe("#FF5733");
+ });
+
+ test("should work without # prefix", () => {
+ expect(normalizeHexForDisplay("000")).toBe("#000000");
+ expect(normalizeHexForDisplay("fff")).toBe("#FFFFFF");
+ expect(normalizeHexForDisplay("FFFFFF")).toBe("#FFFFFF");
+ });
+
+ test("should return null for invalid hex", () => {
+ expect(normalizeHexForDisplay("invalid")).toBeNull();
+ expect(normalizeHexForDisplay("#GGGGGG")).toBeNull();
+ expect(normalizeHexForDisplay("#12")).toBeNull();
+ expect(normalizeHexForDisplay("#1234567")).toBeNull();
+ expect(normalizeHexForDisplay("")).toBeNull();
+ });
+ });
+
+ describe("normalizeHexInput", () => {
+ test("should add # prefix if missing", () => {
+ expect(normalizeHexInput("000000")).toBe("#000000");
+ expect(normalizeHexInput("fff")).toBe("#FFF");
+ expect(normalizeHexInput("FFFFFF")).toBe("#FFFFFF");
+ });
+
+ test("should keep # prefix if present", () => {
+ expect(normalizeHexInput("#000000")).toBe("#000000");
+ expect(normalizeHexInput("#fff")).toBe("#FFF");
+ expect(normalizeHexInput("#FFFFFF")).toBe("#FFFFFF");
+ });
+
+ test("should convert to uppercase", () => {
+ expect(normalizeHexInput("#ffffff")).toBe("#FFFFFF");
+ expect(normalizeHexInput("#abc")).toBe("#ABC");
+ expect(normalizeHexInput("abc")).toBe("#ABC");
+ });
+
+ test("should remove invalid characters", () => {
+ expect(normalizeHexInput("#FF-33")).toBe("#FF33");
+ expect(normalizeHexInput("FF 33")).toBe("#FF33");
+ expect(normalizeHexInput("#123!@#456")).toBe("#123#45");
+ });
+
+ test("should trim whitespace", () => {
+ expect(normalizeHexInput(" #fff ")).toBe("#FFF");
+ expect(normalizeHexInput(" fff ")).toBe("#FFF");
+ });
+
+ test("should limit to 7 characters (# + 6 digits)", () => {
+ expect(normalizeHexInput("#123456789")).toBe("#123456");
+ expect(normalizeHexInput("123456789")).toBe("#123456");
+ });
+
+ test("should handle empty string", () => {
+ expect(normalizeHexInput("")).toBe("");
+ expect(normalizeHexInput(" ")).toBe("");
+ });
+ });
+
+ describe("getRelativeLuminance", () => {
+ test("should calculate luminance for black", () => {
+ const black: RGB = { r: 0, g: 0, b: 0 };
+ const luminance = getRelativeLuminance(black);
+ expect(luminance).toBeCloseTo(0, 5);
+ });
+
+ test("should calculate luminance for white", () => {
+ const white: RGB = { r: 255, g: 255, b: 255 };
+ const luminance = getRelativeLuminance(white);
+ expect(luminance).toBeCloseTo(1, 5);
+ });
+
+ test("should calculate luminance for gray", () => {
+ const gray: RGB = { r: 128, g: 128, b: 128 };
+ const luminance = getRelativeLuminance(gray);
+ expect(luminance).toBeGreaterThan(0);
+ expect(luminance).toBeLessThan(1);
+ });
+
+ test("should calculate luminance for specific colors", () => {
+ const red: RGB = { r: 255, g: 0, b: 0 };
+ const redLuminance = getRelativeLuminance(red);
+ expect(redLuminance).toBeGreaterThan(0);
+ expect(redLuminance).toBeLessThan(1);
+
+ const green: RGB = { r: 0, g: 255, b: 0 };
+ const greenLuminance = getRelativeLuminance(green);
+ expect(greenLuminance).toBeGreaterThan(redLuminance);
+ });
+ });
+
+ describe("getContrastRatio", () => {
+ test("should calculate maximum contrast (black on white)", () => {
+ const black: RGB = { r: 0, g: 0, b: 0 };
+ const white: RGB = { r: 255, g: 255, b: 255 };
+ const ratio = getContrastRatio({ color1: black, color2: white });
+ expect(ratio).toBeCloseTo(21, 1);
+ });
+
+ test("should calculate minimum contrast (same colors)", () => {
+ const gray: RGB = { r: 128, g: 128, b: 128 };
+ const ratio = getContrastRatio({ color1: gray, color2: gray });
+ expect(ratio).toBeCloseTo(1, 2);
+ });
+
+ test("should calculate contrast for #767676 on #FFFFFF", () => {
+ const gray: RGB = { r: 118, g: 118, b: 118 };
+ const white: RGB = { r: 255, g: 255, b: 255 };
+ const ratio = getContrastRatio({ color1: gray, color2: white });
+ expect(ratio).toBeCloseTo(4.54, 2);
+ });
+
+ test("should be symmetric (order doesn't matter)", () => {
+ const color1: RGB = { r: 100, g: 150, b: 200 };
+ const color2: RGB = { r: 200, g: 100, b: 150 };
+ const ratio1 = getContrastRatio({ color1, color2 });
+ const ratio2 = getContrastRatio({ color1: color2, color2: color1 });
+ expect(ratio1).toBe(ratio2);
+ });
+ });
+
+ describe("checkWCAGCompliance", () => {
+ test("should pass all levels for high contrast (21:1)", () => {
+ const result = checkWCAGCompliance(21);
+ expect(result.ratio).toBe(21);
+ expect(result.aa.normal).toBe(true);
+ expect(result.aa.large).toBe(true);
+ expect(result.aa.graphicalObjects).toBe(true);
+ expect(result.aaa.normal).toBe(true);
+ expect(result.aaa.large).toBe(true);
+ });
+
+ test("should pass AA but not AAA normal for 4.5:1", () => {
+ const result = checkWCAGCompliance(4.5);
+ expect(result.ratio).toBe(4.5);
+ expect(result.aa.normal).toBe(true);
+ expect(result.aa.large).toBe(true);
+ expect(result.aa.graphicalObjects).toBe(true);
+ expect(result.aaa.normal).toBe(false);
+ expect(result.aaa.large).toBe(true);
+ });
+
+ test("should pass only AA large for 3:1", () => {
+ const result = checkWCAGCompliance(3);
+ expect(result.ratio).toBe(3);
+ expect(result.aa.normal).toBe(false);
+ expect(result.aa.large).toBe(true);
+ expect(result.aa.graphicalObjects).toBe(true);
+ expect(result.aaa.normal).toBe(false);
+ expect(result.aaa.large).toBe(false);
+ });
+
+ test("should fail all levels for low contrast (1.5:1)", () => {
+ const result = checkWCAGCompliance(1.5);
+ expect(result.ratio).toBe(1.5);
+ expect(result.aa.normal).toBe(false);
+ expect(result.aa.large).toBe(false);
+ expect(result.aa.graphicalObjects).toBe(false);
+ expect(result.aaa.normal).toBe(false);
+ expect(result.aaa.large).toBe(false);
+ });
+
+ test("should pass AAA normal for 7:1", () => {
+ const result = checkWCAGCompliance(7);
+ expect(result.ratio).toBe(7);
+ expect(result.aa.normal).toBe(true);
+ expect(result.aa.large).toBe(true);
+ expect(result.aa.graphicalObjects).toBe(true);
+ expect(result.aaa.normal).toBe(true);
+ expect(result.aaa.large).toBe(true);
+ });
+
+ test("should pass graphical objects for 3:1", () => {
+ const result = checkWCAGCompliance(3);
+ expect(result.ratio).toBe(3);
+ expect(result.aa.graphicalObjects).toBe(true);
+ expect(result.aa.normal).toBe(false);
+ });
+
+ test("should fail graphical objects for 2.9:1", () => {
+ const result = checkWCAGCompliance(2.9);
+ expect(result.ratio).toBe(2.9);
+ expect(result.aa.graphicalObjects).toBe(false);
+ });
+ });
+
+ describe("calculateContrast", () => {
+ test("should calculate contrast for black on white", () => {
+ const result = calculateContrast({
+ foregroundHex: "#000000",
+ backgroundHex: "#FFFFFF",
+ });
+ expect(result).not.toBeNull();
+ if (result) {
+ expect(result.ratio).toBeCloseTo(21, 1);
+ expect(result.aa.normal).toBe(true);
+ expect(result.aa.large).toBe(true);
+ expect(result.aa.graphicalObjects).toBe(true);
+ expect(result.aaa.normal).toBe(true);
+ expect(result.aaa.large).toBe(true);
+ }
+ });
+
+ test("should calculate contrast for #767676 on #FFFFFF", () => {
+ const result = calculateContrast({
+ foregroundHex: "#767676",
+ backgroundHex: "#FFFFFF",
+ });
+ expect(result).not.toBeNull();
+ if (result) {
+ expect(result.ratio).toBeCloseTo(4.54, 2);
+ expect(result.aa.normal).toBe(true);
+ expect(result.aa.large).toBe(true);
+ expect(result.aa.graphicalObjects).toBe(true);
+ expect(result.aaa.normal).toBe(false);
+ expect(result.aaa.large).toBe(true);
+ }
+ });
+
+ test("should return null for invalid hex colors", () => {
+ expect(
+ calculateContrast({
+ foregroundHex: "invalid",
+ backgroundHex: "#FFFFFF",
+ })
+ ).toBeNull();
+ expect(
+ calculateContrast({
+ foregroundHex: "#000000",
+ backgroundHex: "invalid",
+ })
+ ).toBeNull();
+ expect(
+ calculateContrast({
+ foregroundHex: "#12",
+ backgroundHex: "#FFFFFF",
+ })
+ ).toBeNull();
+ expect(
+ calculateContrast({
+ foregroundHex: "#000000",
+ backgroundHex: "#123456789",
+ })
+ ).toBeNull();
+ });
+
+ test("should work with 4 and 5 digit hex colors (treated as 3)", () => {
+ const result = calculateContrast({
+ foregroundHex: "#fff0",
+ backgroundHex: "#12345",
+ });
+ expect(result).not.toBeNull();
+ });
+
+ test("should return null for invalid lengths", () => {
+ expect(
+ calculateContrast({
+ foregroundHex: "#1234567",
+ backgroundHex: "#000",
+ })
+ ).toBeNull();
+ expect(
+ calculateContrast({
+ foregroundHex: "#ffffff00",
+ backgroundHex: "#000",
+ })
+ ).toBeNull();
+ });
+
+ test("should work with 3-digit hex colors", () => {
+ const result = calculateContrast({
+ foregroundHex: "#000",
+ backgroundHex: "#fff",
+ });
+ expect(result).not.toBeNull();
+ if (result) {
+ expect(result.ratio).toBeCloseTo(21, 1);
+ }
+
+ const result2 = calculateContrast({
+ foregroundHex: "#f00",
+ backgroundHex: "#0f0",
+ });
+ expect(result2).not.toBeNull();
+ if (result2) {
+ expect(result2.ratio).toBeGreaterThan(1);
+ }
+ });
+
+ test("should work with hex colors without # prefix", () => {
+ const result = calculateContrast({
+ foregroundHex: "000000",
+ backgroundHex: "FFFFFF",
+ });
+ expect(result).not.toBeNull();
+ if (result) {
+ expect(result.ratio).toBeCloseTo(21, 1);
+ }
+ });
+
+ test("should calculate contrast for low contrast colors", () => {
+ const result = calculateContrast({
+ foregroundHex: "#CCCCCC",
+ backgroundHex: "#FFFFFF",
+ });
+ expect(result).not.toBeNull();
+ if (result) {
+ expect(result.ratio).toBeLessThan(2);
+ expect(result.aa.normal).toBe(false);
+ expect(result.aa.large).toBe(false);
+ }
+ });
+ });
+
+ describe("getContrastDescription", () => {
+ test("should return 'Excellent contrast' for ratio >= 7", () => {
+ expect(getContrastDescription(7)).toBe("Excellent contrast");
+ expect(getContrastDescription(10)).toBe("Excellent contrast");
+ expect(getContrastDescription(21)).toBe("Excellent contrast");
+ });
+
+ test("should return 'Good contrast' for ratio >= 4.5 and < 7", () => {
+ expect(getContrastDescription(4.5)).toBe("Good contrast");
+ expect(getContrastDescription(5)).toBe("Good contrast");
+ expect(getContrastDescription(6.9)).toBe("Good contrast");
+ });
+
+ test("should return 'Minimum contrast for large text' for ratio >= 3 and < 4.5", () => {
+ expect(getContrastDescription(3)).toBe("Minimum contrast for large text");
+ expect(getContrastDescription(3.5)).toBe(
+ "Minimum contrast for large text"
+ );
+ expect(getContrastDescription(4.4)).toBe(
+ "Minimum contrast for large text"
+ );
+ });
+
+ test("should return 'Insufficient contrast' for ratio < 3", () => {
+ expect(getContrastDescription(2.9)).toBe("Insufficient contrast");
+ expect(getContrastDescription(2)).toBe("Insufficient contrast");
+ expect(getContrastDescription(1.5)).toBe("Insufficient contrast");
+ expect(getContrastDescription(1)).toBe("Insufficient contrast");
+ });
+ });
+});
diff --git a/components/utils/wcag-color-contrast.utils.ts b/components/utils/wcag-color-contrast.utils.ts
new file mode 100644
index 0000000..984f2e8
--- /dev/null
+++ b/components/utils/wcag-color-contrast.utils.ts
@@ -0,0 +1,240 @@
+export interface RGB {
+ r: number;
+ g: number;
+ b: number;
+}
+
+export interface ContrastResult {
+ ratio: number;
+ aa: {
+ normal: boolean;
+ large: boolean;
+ graphicalObjects: boolean;
+ };
+ aaa: {
+ normal: boolean;
+ large: boolean;
+ };
+}
+
+interface ContrastDescriptionRule {
+ condition: boolean;
+ result: string;
+}
+
+type HexLength = 3 | 4 | 5 | 6;
+
+export const WCAG = {
+ AA: {
+ NORMAL_THRESHOLD: 4.5,
+ LARGE_THRESHOLD: 3,
+ GRAPHICAL_OBJECTS_THRESHOLD: 3,
+ },
+ AAA: {
+ NORMAL_THRESHOLD: 7,
+ LARGE_THRESHOLD: 4.5,
+ },
+} as const;
+
+// WCAG 2.1 constants - Reference: https://www.w3.org/WAI/GL/wiki/Relative_luminance
+// Note: WCAG 2.x uses 0.03928, while the correct IEC standard uses 0.04045.
+// For 8-bit color values, the difference is not significant. We use 0.03928 to match WCAG 2.x specification.
+const SRGB_GAMMA_THRESHOLD = 0.03928 as const;
+const SRGB_LOW_VALUE_DIVISOR = 12.92 as const;
+const SRGB_GAMMA_OFFSET = 0.055 as const;
+const SRGB_GAMMA_DIVISOR = 1.055 as const;
+const SRGB_GAMMA_EXPONENT = 2.4 as const;
+
+const LUMINANCE_R_COEFFICIENT = 0.2126 as const;
+const LUMINANCE_G_COEFFICIENT = 0.7152 as const;
+const LUMINANCE_B_COEFFICIENT = 0.0722 as const;
+
+const CONTRAST_RATIO_CONSTANT = 0.05 as const;
+
+const HEX_COLOR_PATTERN: RegExp = /^[0-9A-Fa-f]{3,6}$/;
+const INVALID_HEX_CHARS: RegExp = /[^#0-9A-F]/gi;
+
+const parseHexToRgb = (normalizedHex: string, length: HexLength): RGB => {
+ const shouldExpand = [3, 4, 5].includes(length);
+ const expandedLength = shouldExpand ? 3 : length;
+
+ let hexToParse = normalizedHex;
+
+ if (shouldExpand) {
+ hexToParse = hexToParse
+ .substring(0, expandedLength)
+ .split("")
+ .map((char) => char + char)
+ .join("");
+ }
+
+ const r = parseInt(hexToParse.substring(0, 2), 16);
+ const g = parseInt(hexToParse.substring(2, 4), 16);
+ const b = parseInt(hexToParse.substring(4, 6), 16);
+
+ return { r, g, b };
+};
+
+const hexToRgbMapper: Record