Add color contrast checks to custom focus and accent color dialogs

Created on 13 February 2025, 5 months ago

Problem/Motivation

At the moment the user is able to define custom focus and accent colors on admin/appearance/settings/gin. That way it is in no way ensured that the color that is picked here is in any way meeting the minimum color contrast requirements to be compliant with SC1.4.3, SC1.4.11, and/or SC2.4.13.

Discussed and iterated on the issue with @mgifford, @the_g_bomb, @katannshaw, and @drupa11y

Steps to reproduce

  • Try changing the accent color to custom on admin/appearance/settings/gin.

Proposed resolution

  • Add a feedback to the color selection dialog that validates the picked color against the three or six background colors in the current mode (light/dark) the focus or accent color is displayed against.
  • The selected focus color has to have a color contrast of at least 3:1, accent colors have to have a color contrast of at least 4.5:1 against the set of available background colors for the chosen color mode.
  • Provide an immediate feedback if the required color contrast is not met or met
  • .

  • In case a user tries to save the form with the chosen color anyway, provide a confirmation step informing the user again that the color is not meeting the requirements and is not compliant with WCAG2.2 - to make sure the person is really sure about the implications of that step and that this is not a recommended nor advisable step.
  • In case a user is using a compliant custom focus and or custom accent color and is now changing from light to dark mode or the other way around, bring up the custom color dialog and run the compliance check - cuz the odds are high that the chosen color wont meet the requirement for the complementary color mode.

Remaining tasks

User interface changes

API changes

Data model changes

Feature request
Status

Active

Version

4.0

Component

User interface

Created by

🇩🇪Germany rkoller Nürnberg, Germany

Live updates comments and jobs are added and updated live.
  • Accessibility

    It affects the ability of people with disabilities or special needs (such as blindness or color-blindness) to use Drupal.

Sign in to follow issues

Comments & Activities

  • Issue created by @rkoller
  • 🇩🇪Germany rkoller Nürnberg, Germany
  • 🇨🇦Canada mgifford Ottawa, Ontario
  • 🇨🇦Canada mgifford Ottawa, Ontario

    There are 11 Accent colors, 4 different Focus colors, plus Dark/Light modes. That's a lot of combinations. Probably too many combinations to be able to provide good guidance.

    Drupal Core should be providing a good example. We have as a community committed to striving to meet WCAG 2.2 AA standards. This is especially true of Core and Drupal CMS.

    #0750E6 / #FFF
    #2E6DD0 / #FFF
    #4300BF / #FFF
    #5B00FF / #FFF
    #0F857F / #FFF - Fail (could we not just use #088488)
    #00875F / #FFF
    #D12E70 / #FFF
    #D8002E / #FFF
    #DA6303 / #FFF - Fail (Could we not just use #B56026)
    #111111 / #FFF

  • 🇩🇪Germany rkoller Nürnberg, Germany

    ? this issue is about adding color contrast checks for the color chosen in the custom color widget (against all relevant colors depending if the light or dark mode is chosen)

    and what are those color combinations of yours refer to? focus colors accents colors? all the combinations are covered in the google sheets.

  • 🇨🇦Canada mgifford Ottawa, Ontario

    Ya, I think I filed this in the wrong place. I can move it, but what is the issue.

  • 🇩🇪Germany rkoller Nürnberg, Germany

    as i'Ve said all the combinations are already listed in the google sheet.

    focus:
    https://docs.google.com/spreadsheets/d/1won35PxhRFexJYE8FmZ4DCNTo7xEAxC8...

    accent colors:
    https://docs.google.com/spreadsheets/d/1won35PxhRFexJYE8FmZ4DCNTo7xEAxC8... (and the components tab contains all the components from the rest of the issues)

    and it is a bit more complex since it is not just a single background color but several for light and several for dark mode plus the colors are semi transparent and for the focus outline you have two colors due to the fact how the focus is currently built in gin.

  • 🇨🇦Canada mgifford Ottawa, Ontario

    I suspect that some JS like this will be needed:

    (function () {
      const MIN_CONTRAST = 4.5;
      const BACKGROUNDS = [
        { hex: '#ffffff', name: 'white' },
        { hex: '#2A2A2D', name: 'dark grey' }
      ];
    
      const fields = [
        {
          inputId: 'edit-accent-color',
          warningId: 'accent-warning',
        },
        {
          inputId: 'edit-focus-color',
          warningId: 'focus-warning',
        },
      ];
    
      function hexToRgb(hex) {
        const val = hex.replace('#', '');
        return {
          r: parseInt(val.slice(0, 2), 16) / 255,
          g: parseInt(val.slice(2, 4), 16) / 255,
          b: parseInt(val.slice(4, 6), 16) / 255,
        };
      }
    
      function luminance({ r, g, b }) {
        const adjust = (c) =>
          c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
        return (
          0.2126 * adjust(r) + 0.7152 * adjust(g) + 0.0722 * adjust(b)
        );
      }
    
      function contrastRatio(l1, l2) {
        const [a, b] = [l1, l2].sort((x, y) => y - x);
        return (a + 0.05) / (b + 0.05);
      }
    
      function updateWarning(field) {
        const input = document.getElementById(field.inputId);
        const existing = document.getElementById(field.warningId);
        if (existing) existing.remove();
    
        const val = input.value;
        if (!/^#[0-9a-fA-F]{6}$/.test(val)) return;
    
        const fgLum = luminance(hexToRgb(val));
        const fails = BACKGROUNDS.filter((bg) => {
          const bgLum = luminance(hexToRgb(bg.hex));
          return contrastRatio(fgLum, bgLum) < MIN_CONTRAST;
        });
    
        if (fails.length) {
          const msg = document.createElement('div');
          msg.id = field.warningId;
          msg.style.color = 'red';
          msg.style.marginTop = '0.25rem';
          msg.textContent =
            '⚠️ Low contrast against: ' +
            fails.map((f) => f.name).join(', ') +
            ` (min ${MIN_CONTRAST}:1)`;
          input.parentNode.appendChild(msg);
        }
      }
    
      document.addEventListener('DOMContentLoaded', () => {
        fields.forEach((field) => {
          const input = document.getElementById(field.inputId);
          if (input) {
            input.addEventListener('input', () => updateWarning(field));
            updateWarning(field); // initial check
          }
        });
      });
    })();
Production build 0.71.5 2024