Scale your URCap UI with the zoom level

From PolyScope X 10.15 the user interface is sized in rem, and PolyScope X sets the root font-size of the page per device. On the TP7 Teach Pendant the UI starts at 135% (a root font-size of 21.6px instead of 16px), so text, icons and touch targets are larger on its denser display.

Your URCap’s frontend contributions are rendered as custom elements in the same page as PolyScope X. That means:

  • Sizes in rem in your URCap already follow the zoom level.

  • Sizes in px do not. On a TP7, a URCap styled in px looks noticeably smaller than the PolyScope X UI around it.

This guide shows how to make your URCap scale with PolyScope X by converting px to rem when you build, without rewriting your stylesheets.

Important

Do not set the zoom level from your URCap. Do not set font-size on html, set the --font-base-size CSS variable, or use CSS zoom or transform: scale() on the page. PolyScope X owns the root font-size, and changing it rescales all of PolyScope X, not just your URCap. CSS zoom also breaks the positioning of dialog boxes and dropdowns.

How it works

1rem equals the root font-size of the page. PolyScope X sets the root font-size to 16px at 100%, so a stylesheet where every px value is replaced by px / 16 rem looks exactly the same at 100%, and grows with the zoom level on larger settings. The conversion runs as a PostCSS step in the Angular build, so you keep writing px in your .scss files.

There are two parts:

  1. Convert the stylesheets that your own build compiles (step 1).

  2. Convert the component styles of the UI Design Library (@universal-robots/ui-angular-components), which are shipped already compiled (step 2).

The steps assume the Angular frontend created by the URCap generator, which builds with ngx-build-plus.

Step 1: convert your own styles

Install the PostCSS plugin in your frontend project:

npm install --save-dev postcss-pxtorem

Create postcss.config.json in the root of the frontend project, next to angular.json:

{
    "plugins": {
        "postcss-pxtorem": {
            "rootValue": 16,
            "unitPrecision": 5,
            "propList": ["*"],
            "mediaQuery": false,
            "minPixelValue": 2
        }
    }
}

The Angular build finds this file on its own and runs the plugin on every stylesheet it compiles: component styles, global styles, and the SCSS of the UI Design Library that you @use.

Option

Why

rootValue: 16

16px is 1rem at 100%, so your URCap looks unchanged at the default zoom level.

propList: ["*"]

Convert every property, including custom properties (CSS variables).

mediaQuery: false

Leave @media breakpoints in px, so the layout breakpoints do not move with the zoom level.

minPixelValue: 2

Leave 1px in px, so hairline borders stay sharp.

Note

The file must be named postcss.config.json (or .postcssrc.json). Angular does not read postcss.config.js.

Step 2: convert the UI Design Library components

The components of the UI Design Library (ur-button, ur-dropdown, ur-tile-button and so on) are shipped in the npm package as compiled JavaScript with their CSS embedded as strings. Step 1 never sees these styles, so without step 2 your own layout scales but the ur-* components inside it keep their px sizes.

Note

This step is a workaround for PolyScope X 10.15. If a later version of @universal-robots/ui-angular-components ships its component styles in rem, the step is no longer needed and you can delete the two files below and the extraWebpackConfig lines. The workaround is safe to keep either way: values that are already in rem are left untouched.

Create a build folder in the frontend project with the following two files.

build/vendor-pxtorem-loader.js runs postcss-pxtorem over the component styles embedded in the package:

// Webpack loader: runs postcss-pxtorem over the precompiled component styles embedded in
// Angular packages (the `styles: ["..."]` arrays in their fesm2022 bundles), so the ur-*
// components scale with the PolyScope X zoom level.
// Not needed if your version of @universal-robots/ui-angular-components ships its component styles in rem.
const path = require('path');
const postcss = require('postcss');
const pxtorem = require('postcss-pxtorem');

// Same options as your own stylesheets (postcss.config.json).
const { plugins } = require(path.resolve(__dirname, '../postcss.config.json'));
const processor = postcss([pxtorem(plugins['postcss-pxtorem'])]);

// Angular partial-compilation output: `styles: ["css", "css"]` with double-quoted string literals.
const STYLES_ARRAY = /styles: \[((?:\s*"(?:[^"\\]|\\.)*",?)+)\s*\]/g;
const STRING_LITERAL = /"(?:[^"\\]|\\.)*"/g;

function convert(literal) {
    try {
        const css = JSON.parse(literal);
        return JSON.stringify(processor.process(css, { from: undefined }).css);
    } catch {
        // Not a plain JSON-compatible string: leave that style unconverted rather than fail the build.
        return literal;
    }
}

module.exports = function vendorPxToRemLoader(source) {
    return source.replace(STYLES_ARRAY, (_, literals) => {
        const converted = literals.match(STRING_LITERAL).map(convert);
        return `styles: [${converted.join(', ')}]`;
    });
};

build/extra-webpack.config.js applies the loader to the UI Design Library, and to ng-select, which ur-dropdown uses for its option list:

// Merged into the Angular webpack config by ngx-build-plus (angular.json `extraWebpackConfig`).
const path = require('path');

module.exports = {
    module: {
        rules: [
            {
                // Runs before Angular's own processing, while the `styles` arrays are still plain strings.
                enforce: 'pre',
                test: /[\\/](@universal-robots[\\/]ui-angular-components|@ng-select[\\/]ng-select)[\\/]fesm2022[\\/].*\.mjs$/,
                use: [path.resolve(__dirname, 'vendor-pxtorem-loader.js')],
            },
        ],
    },
};

Register the webpack configuration in angular.json. Add it to the options of both the build target and the serve target: the dev server reads its own options, not the ones of the build target.

"build": {
    "builder": "ngx-build-plus:browser",
    "options": {
        "singleBundle": true,
        "extraWebpackConfig": "build/extra-webpack.config.js",
        ...
    }
},
"serve": {
    "builder": "ngx-build-plus:dev-server",
    "options": {
        "singleBundle": true,
        "extraWebpackConfig": "build/extra-webpack.config.js",
        ...
    }
}

Step 3: build and check

Angular keeps a build cache that does not notice changes to postcss.config.json or the loader. Clear it whenever you add or change either, or some stylesheets stay in px:

rm -rf .angular/cache
npm run build

To check the result, search the built bundle for rem. For example, a 116px width in your .scss becomes 7.25rem.

Sizes the build cannot convert

The conversion only reaches CSS. Check your URCap for these cases, which stay in px and do not scale:

  • Icons and images sized by their own width and height. An <img> showing an SVG with width="60" height="60", or <img width="48" height="48">, keeps that size. Give the image a size in your stylesheet, for example .tile-icon { width: 60px; height: 60px; }, so it is converted and scales.

  • Inline styles. style="width: 200px" in a template and bindings like [style.width.px]="200" are not converted. Move the size to a stylesheet, or bind rem values.

  • Sizes computed in TypeScript. Code that measures or positions elements in px gets the wrong result at other zoom levels. Work in rem, or read the current root font-size with parseFloat(getComputedStyle(document.documentElement).fontSize).

  • Angular Material. Its component styles are shipped compiled, like the UI Design Library’s, and are not covered by step 2.

1px borders and @media breakpoints are left in px on purpose (see the options in step 1).

Test it

You can preview the TP7 zoom level in the simulator or in any browser. Open the browser developer tools on the PolyScope X page and run:

document.documentElement.style.setProperty('--font-base-size', '21.6');

This sets the root font-size to 21.6px (135%) until the page is reloaded. Check that your URCap now scales like the PolyScope X UI around it, that icons grow with the text, and that dialog boxes and dropdowns still open attached to their fields.

Compatibility

PolyScope X versions before 10.15 use a root font-size of 16px, so a URCap built this way looks the same on them as before. You can ship the same build for all PolyScope X versions.