To better understand the accessibility challenge, let us explore how browser zoom functionality works. You may already be familiar with this feature, using keyboard shortcuts like Command / Ctrl + or Command / Ctrl — to scale all content within a window. When you increase the zoom level beyond 100%, the viewport’s height and width proportionally decrease, while the content is blown up to fit the larger window.As part of our accessibility testing strategy, we were using browser zoom to test the usability of our pages both on desktop and mobile sizes. Desktop testing showed that our pages did relatively well at the 200% zoom level with our responsive web approach across the site. We saw fewer issues in the overall user experience when compared to mobile web.This works well on desktop, where we serve a smaller breakpoint (e.g., wide to compact) and the viewport is relatively spacious. However, the limitations of browser zoom become more pronounced on mobile web, where the viewport is smaller. If we were to scale the content in a mobile viewport, it would have to fit into a viewport that is half the width and half the height of the original. This can result in significant accessibility issues, as the text and UI elements become extremely difficult to read and interact with. As shown in the image on the right, the ability to view even a single listing within a screen’s worth of space is not possible without scrolling, leading to a frustrating experience.Airbnb’s homepage shown at browser zoom 100% on the left, and the same screen shown at 200% showing the search and categories are cut off entirely and not able to even see the first listing.Font scaling is the term we’ll use to describe the ability to adjust text size independently of overall page zoom. Unlike browser zoom, which scales all content proportionally, Font Scaling applies only to the text elements on the page. This allows users to customize the font size to their preferred reading size without affecting much of layout or responsiveness of the rest of the content.Font Scaling, is also the term we will use for scaling the font based on a user’s preferred size. Unlike zoom, this setting will be applied to all sites. Below is an example of how the font scaling applies to just the text on the screen, showing that the only scale of the text increases, instead of all the content.Video Description: Airbnb text is scaled by setting the font size on arc browser, showing the scaling from 16px to 32px.This concept of independent font scaling is similar to the Dynamic Type feature on iOS, as we discussed in our blog post “Supporting Dynamic Type at Airbnb”. Dynamic Type allows users to set a preferred system-wide text size, which then automatically adjusts the font size across all compatible apps.Considering our existing strategies for accessibility on iOS, incorporating font scaling (vs zoom scaling) into our web accessibility approach was a natural next step to help add parity in approaches across our platforms.Now that we understand why font scaling is so powerful for mobile web, we should focus on why we might choose one CSS length unit over another for supporting font scaling. In this blog post we are only going to focus on px, em and rem but there are other units as well. CSS length units are connected to font scaling because they determine how text and other elements are sized on a web page. Some length units are fixed, meaning they don’t change based on the user’s font size settings, while others are relative, meaning they scale proportionally with the font size.Let’s take a deep look at 3 CSS length units and how they relate to font scaling:px units are the most commonly used on the web, theoretically they should represent one pixel on the screen. They are a fixed unit meaning the rendered value does not change.em units however are a relative unit that are based on the parent element’s font size. The name ‘em’ comes from the width of the capital letter ‘M’ in a given typeface, which was traditionally used as the reference point for font sizes. 1 em unit is equal to the height of the current font size, roughly 16px at the default value. em units scale proportionally, so they can be affected by their parent’s font sizesrem units, short for “root em”, are similar to em units in that they are proportional to font size, but they only use the root element (the html element) to calculate their font size. This means that rem units offer font scaling, but are not affected by their parent’s font size.The choice between em and rem units often comes down to the level of control and predictability required for font scaling. While em units can be used, they can lead to cascading font size changes that may be difficult to manage, especially in complex layouts. In contrast, rem units provide a more consistent and predictable approach to font scaling, as they are always relative to the root element’s font size.This is illustrated in the CodePen example, where the different font scaling behaviors of px, em, and rem units are demonstrated. In situations where font scaling is a critical requirement, such as the Airbnb example mentioned, the use of rem units can be a more reliable choice to ensure a consistent and maintainable font scaling solution.Relative units like rem can be used anywhere a fixed unit like px can be used. However, indiscriminate use of rem units across all properties can lead to unwanted scaling behavior and increased complexity.In the case of Airbnb, the team decided to prioritize the use of rem units specifically for font scaling, rather than scaling all elements proportionally. This targeted approach provided the key benefit of consistent text scaling, without the potential downsides of scaling every aspect of the layout.The rationale behind this decision was twofold:Scaling everything using rem units would have been similar to Browser Zoom and potentially introduced unintended layout issues,The primary focus was on providing a mobile-friendly font scaling solution. By targeting font sizes with rem units, the team could ensure that the most important content — the text — scaled appropriately.Moving from pixel-based values to rem units as a company-wide change in CSS practice can be a significant challenge, especially when working across multiple teams. The time and effort required to educate designers and frontend developers on the new approach, and to have them convert their existing pixel-based values to rem units, can be a significant barrier to adoption. To address this, the Airbnb team decided to focus on automating the unit conversion process as much as possible, enabling a more seamless transition to the new rem-based system.Instead of requiring designers to have to think of new units or introduce some conversion for web only, we decided to continue to author our CSS in px units. This reduced the amount of training required for teams to start using rem units out the gate.One area we did focus on with our design teams was starting to test their designs using font scaling by leveraging the Text Resizer — Accessibility Checker to help simulate what a design might look like at 2X the font size. This tool helped us spot problems earlier into the design process.Airbnb is in the process of transitioning from React-with-Styles to a newer approach using Linaria. While the adoption of Linaria was progressing quickly, we recognized the need to support both styling systems for a consistent experience. Managing the conversion across these two different CSS-in-JS systems posed an additional challenge.LinariaBy leveraging Linaria’s support for CSS custom properties, the team was able to create new typography theme values that automatically converted the existing pixel-based values to their rem equivalents. This approach allowed the team to introduce the new rem-based theme values in a centralized manner, making them available to child elements. This gave the team the ability to override the rem values on a per-page basis, providing the necessary flexibility during the transition process.import { typography } from ‘./site-theme’;// Loops through the CSS Vars we use for typography and converts them// from px to rem units.const theme: css`${getCssVariables({ typography: replacePxWithREMs(typography) })}// Changes from:// – body-font-size: 16px;// To// – body-font-size: 1rem; `;// Use the class name generated from linaria to override the theme // variables for the children of this component.const RemThemeLocalProvider: React.FC = ({ children }) => {const cx = useCx();return <div className={linariaClassNames.theme)}>{children}</div>;};tyAlthough this approach helped us convert most of the font scaling properties, there were many places in our code that we used pxbased values outside the theme. Linaria’s support for post-CSS plugins made solving these areas relatively easy. We leveraged postcss-pxtorem to help target those values more easily. We started by using an allow list, so that we could carefully apply this change to a smaller set of early adopting pages.It was important that we provided an escape hatch when there was some reason for front-end engineers needing to use px units. Luckily we were able to provide this by using a different casing for the px value like shown below./* `px` is converted to `rem` */.convert {font-size: 16px; /* converted to 1rem */}/* `Px` or `PX` is ignored by `postcss-pxtorem` but still accepted by browsers */.ignore {font-size: 200Px;font-size: clamp(16Px, 2rem, 32Px);}React with StylesA good amount of our frontend code still uses react-with-styles, so we had to find another way to support these cases with an easy conversion. Through this we created a simple Higher-Order component that made the conversion pretty straightforward. First we created a wrapper for the withStyles function like below, and gave the ability to avoid conversion as well.export const withRemStyles = (styleFn?: Nullable<(theme: Theme) => Styles>,options?: WithStylesOptions & { disableConvertToRemUnits?: boolean },) => {const disableConvertToRemUnits = getDisableConvertToRemUnits(options);// If conversion is disabled, just return the original withStyles functionif (disableConvertToRemUnits) {return _withStyles(styleFn, options);}// Otherwise, wrap the original style function with a new function // that converts px to remreturn _withStyles((theme: Theme) => {if (styleFn) {const styles = styleFn(theme);const remStyles = convertToRem(styles);return remStyles;}return {};}, options);};Then the convertToRem will look through the keys and values and map a converted value for any of the font sizing attributes. This allowed us to automate the conversion process in a more straightforward way.With these two challenges out of the way, we can start testing our components to verify if there are any major issues we might need to resolve before rolling out. In our component documentation and tooling, we built an internal plugin to allow for easier testing by setting the font-size on the html element directly to test with font scaling.Screenshot testing has helped our teams catch visual regressions. Adding support to allow for setting additional screenshots at different root font sizes has helped our product teams review what the component looks like at different font scales. To do this, we allow for adding additional font sizes to be set when capturing the screenshots so you don’t have to create new component variations just for font scaling.Supporting font scaling for Mobile Safari was more difficult. Unlike other browsers, there is not a font size preference available in Mobile Safari. However, they have released support for their own font: -apple-system-body but there are some important considerations.Since macOS High Sierra (10.13), desktop Safari also supports the font preference, but there is not an easy “font size” configuration available in MacOS. Because there can be unexpected behavior on desktop Safari, so we used a @supports statement to prevent this. The code below will only target Mobile Safari.// Apple’s Dynamic Type requires this font family to be used// Only target iOS/iPadOS@supports (font: -apple-system-body) and (-webkit-touch-callout: default) {:root {font: -apple-system-body;}}Another consideration is that the “100%” default font size selected does not equal the standard font size of 16px, but rather 17px. This is a very subtle difference, but it is critical for the design quality bar we aim to achieve at Airbnb. So to resolve this issue, we ended up using an inline head script to normalize the value, by placing it early into the page execution we avoided seeing a change in font size.(() => {// don’t do anything if the browser doesn’t match the supports statementif (!CSS.supports(‘(font: -apple-system-body) and (-webkit-touch-callout: default)’)) return;// Must create an element since the root element styles are not yet parsed.const div = document.createElement(‘div’);div.setAttribute(‘style’, ‘font: -apple-system-body’);// Body is not available yet so this has to be added to the root elementdocumentElement.appendChild(div);const style = getComputedStyle(div);if (style.fontSize === ’17px’) {documentElement.style.setProperty(‘font-size’, ’16px’);}documentElement.removeChild(div);})();Then when the page loads we use a resize observer to detect if the value changes again to unset or set the font-size property on the html element. This helps us still support scalable fonts, but not have a significant impact on the default font size (100%).Supporting scalable fonts is an investment that should make a dramatic difference for our Hosts and guests with low vision and anyone who benefits from larger font sizes and control over their browsing experience. Below are two examples of the home page showing how the default font size (16px) appears to someone who has blurry vision and what it looks like by doubling the font size (32px). The second image is far more legible and usable.