cssfont-familycss-variables

why does font-family fallback to user-agent when css variable is defined but font is not available


The situation is when using css variables with font-family, the defined default font-family is not the fallback font-family.

We have a css variable defined for --myFontFamily.

When the font-family defined by the css variable --myFontFamily is not available on the site, the user-agent font-family is utilized.

The expectation is that our default font-family will be the fallback when the css variable font-family is not available. For clarity --myFontFamily is defined. The value is a custom font that does not exist on the site.

I have reviewed webstyle guide

I reviewed css vars

we utilize the shadowDOM for our content so we utilize the sudo class for :host {} to ensure proper style cascading for elements inside the shadowDOM.

css variable will be injected into the head of the html within a style tag

elements that do not define font-family will inherit the font-family defined within :host {}

<style type="text/css" class="custom-site-style" id="custom-site-styles">
    :root {
      --myFontFamily: CustomFontButNotOnTheSitePage;
    }
  </style>


<div>
  <!-- there would be a shadow-root here -->
  <style>
    :host {
      font-family: Arial, Helvetica;
    }

    .my-content {
      font-family: var(--myFontFamily);
    }
  </style>

  <div> Font Family is Arial as expected </div>
  <div class="my-content">  User Agent font-family is used.  Expectation is for font family to default to Arial because the css variable is defined but the font does not exist on the site page.  We have a defined font-family that is inherited by other elements.
  </div>
</div>

I have experimented and found that this will work and fallback to the default font-family as expected. However this doesn't explain why the user-agent is the fallback in the above example.

<div>
  <style>
    :host {
      --defaultFontFamily: Arial, Helvetica;
      font-family: Arial, Helvetica;

    }

    .my-content {
      font-family: var(--myFontFamily), var(--defaultFontFamily);
    }
  </style>

As requested. A true working example.

<html>
    <head>
        <style>
            body {
                font-family: 'Times New Roman';
            }
        </style>
        <style type="text/css" class="custom-site-style" id="custom-site-styles">
            :root {
              --myFontFamily: CustomFontButNotOnTheSitePage;
            }
          </style>
    </head>

    <body>
        <div class="inject-here">

        </div>
    </body>
    <script type="text/javascript">
        (function () {// mimics proprietary production code
            const content = document.querySelector('.inject-here');
            if (!content.shadowRoot) {
                const shadow = content.attachShadow({ mode: 'open' });
                
                // demonstrates the issue observed
                // when the custom variable has defined a font-family that does not exist on the page
                // the default font-family will cascade from the page / user agent 
                // rather than from the defined styles in :host {}
                const hostStyles = `
                    :host {
                        font-family: Arial, Helvetica;
                        /*demonstrates the :host is correct*/
                        color: blue; 
                        font-weight: 500;
                    }

                    .my-content {
                        font-family: var(--myFontFamily);
                        color: black; /*demonstrates cascade works*/
                    }`;

                // // working example of explicit default font-family will be used as the fallback
                // // uncomment this to see it work properly
                // const hostStyles = `
                //     :host {
                //         --myDefaultFontFamily: Arial, Helvetica;
                //         font-family: var(--myDefaultFontFamily);
                //         /*demonstrates the :host is correct*/
                //         color: blue; 
                //         font-weight: 500;
                //     }

                //     .my-content {
                //         font-family: var(--myFontFamily), var(--myDefaultFontFamily);
                //         color: black; /*demonstrates cascade works*/
                //     }`;

                

                shadow.innerHTML = `<style>
                    ${hostStyles}
                </style>
                <div> Content font-family will be Arial or Helvetica</div>
                <div class="my-content">User Agent font-family will be used here.  Expected that the :host font-family would be used</div>`;
            }
        }());

        
        
    </script>
</html>

Solution

  • The CSS vars are irrelevant to the problem.
    Replacing the var with the actual value gives the following CSS which is simpler to reason with:

    :host {
      font-family: Arial, Helvetica;
    }
    .my-content {
      font-family: DefinedButNotOnTheSitePage;
    }
    

    font-family: DefinedButNotOnTheSitePage; is a syntactically valid CSS declaration, so the browser will apply it to .my-content, ‘replacing’ the existing font-family inherited to the element just like any other CSS property.

    Only if it was an invalid declaration would the browser ignore it and have Arial, Helvetica inherited.

    Since a font called DefinedButNotOnTheSitePage cannot be found, and the CSS declaration has no fallback fonts specified, the user agent's defined fallback font is used.


    For example,

    :host {
      color: red;
    }
    .my-content {
      color: yellow;
    }
    

    does not make .my-content orange. CSS declarations are not combined together; .my-content will be yellow.

    However,

    :host {
      color: red;
    }
    .my-content {
      color: abcdefghi;
    }
    

    .my-content will be red because color: abcdefghi; is invalid: there is no color abcdefghi at the time of reading the CSS.

    For color, the spec says what values are possible as named colors:

    https://developer.mozilla.org/en-US/docs/Web/CSS/named-color

    If the color value is a name but is not in this list, the CSS declaration is immediately invalid and ignored.

    However font-family does not have a fixed list of possible values in spec.

    font-family = 
      [ <family-name> | <generic-family> ]#  
    

    https://developer.mozilla.org/en-US/docs/Web/CSS/font-family

    Therefore at the point of reading the CSS any string that could be a family-name is allowed.

    It is only later when looking up the font given that it cannot be found. That doesn't make the CSS declaration invalid and ignored — it's ‘too late’ for that.