Skip to content
SiteEmail

A scalable UI is a UI that can dynamically scale up or down to fit bigger or smaller screens. A well-designed responsive interface ensures that components are not distorted or malformed from the scaling, and allows adjustments to fit the screen even if the aspect ratio changes.

This guide explores the mechanics of scalable layouts in Gameface. We will cover the pitfalls of hardcoded pixels, how to leverage viewport units and REM scaling , and alternative approaches like calculating and applying dynamic scale factors based on a reference resolution.

When building a UI, it is tempting to measure and position elements using strict pixel (px) values based on a single target screen dimension. The screen size for which the UI was originally designed is known as the reference resolution .

However, pixels are absolute units. If your reference resolution is 1080p (1920x1080), a hardcoded 300px minimap will look perfectly sized. But when the game is rendered on a 4K screen (3840x2160), the screen has exactly twice the number of pixels in both width and height. Because the minimap remains strictly 300px, it will appear physically half the size on the 4K screen, making it incredibly difficult for the player to read.

The most direct way to scale layout containers proportionally is by using Viewport Units: vw (viewport width) and vh (viewport height).

  • 1vw is equal to 1% of the total screen width.
  • 1vh is equal to 1% of the total screen height.

Because these units are intrinsically tied to the screen’s actual dimensions, an element sized with vw or vh will always occupy the exact same relative physical space on the screen, whether it is running on a 720p handheld or a 4K monitor.

hud.css
/* This container will always take up exactly 30% of the screen's width
and 100% of its height, regardless of the user's resolution.
*/
.inventory-panel {
width: 30vw;
height: 100vh;
background: rgba(0, 0, 0, 0.8);
}
/* Margins and spacing can also use viewport units to maintain
consistent relative spacing from the edge of the screen.
*/
.minimap {
width: 15vw;
height: 15vw; /* Using vw for both ensures the map stays a perfect square */
margin-top: 2vh;
margin-right: 2vw;
}

While viewport units (vw and vh) are excellent for massive layout blocks, using them for every single padding, margin, border, and font size can become tedious and hard to maintain.

Instead, the most robust way to build a scalable UI is to author your internal component dimensions using rem units. The rem unit is a size unit used in CSS that is tied directly to the root (html) element’s font size.

If you set the size of all UI elements using rem units, you can easily resize them globally by changing the font size of the HTML element. By scaling this single root value, the entire UI-text, buttons, margins, and containers-scales up or down in perfect unison.

scalable-components.css
/* If the root font-size is 10px, this button is 200px wide.
If the root font-size changes to 20px, this button dynamically grows to 400px wide. */
.action-button {
width: 20rem;
height: 5rem;
padding: 1rem;
font-size: 1.5rem;
}

To make this actually responsive to the screen, we need a way to automatically adjust that root font size based on the player’s resolution. There are two main ways to achieve this: dynamically via JavaScript , or statically via CSS .

The Dynamic Scale Factor Method (JavaScript)

Section titled “The Dynamic Scale Factor Method (JavaScript)”

The industry standard for game UI is to design the interface for a specific reference resolution (e.g., 1920x1080).

A user interface is scaled using a scale factor, which is a numeric value that determines how the size of the UI element will be affected. Comparing the current resolution to the reference resolution gives the scale factor.

When calculating the scale factor, the sides which are used determine the end result of the scaling. You can compare only the width, only the height, or both. For standard widescreen games, scaling based on the screen’s height is often preferred to ensure vertical UI panels don’t get cut off.

Here is how you can use JavaScript to dynamically calculate this scale factor and apply it to the root element.

scale-manager.js
// Grab the root HTML element to apply our dynamic font-size
const rootElement = document.getElementsByTagName('html')[0];
function resize() {
// Get the current physical height of the screen
const windowHeight = window.innerHeight;
// Compare the current height to our reference height of 1080
// If the player is on a 2160p (4K) screen, the factor is 2.0
const scaleFactorY = windowHeight / 1080;
// Multiply our base CSS design size (e.g., 16px) by the scale factor
const fontSize = 16 * scaleFactorY;
// Apply the newly calculated font size to the root element
rootElement.style.fontSize = `${fontSize}px`;
}
// Run once on initialization to set the initial scale
resize();
// Update the scale dynamically whenever the window resizes
window.addEventListener('resize', resize);

If you want to avoid the JavaScript bottleneck entirely, you can manually calculate the Viewport Height (vh) equivalent of your base pixel size and apply it directly to your CSS.

Assuming our reference wireframe was designed at 1920x1080, and we want our base 1rem to equal 16px at that resolution, we calculate the viewport height percentage: (16px / 1080px) * 100 = 1.4814vh.

Add this rule to the top of your index.css:

index.css
html {
font-size: 1.4814vh;
}

With this single rule applied, an element with width: 2rem will render at exactly 32px on a 1080p screen, but will dynamically scale up on a 1440p or 4K screen, maintaining its relative proportion to the height of the window.

There is no “one-size-fits-all” solution when it comes to building a responsive game UI. All of the aforementioned approaches have their benefits depending on your UI’s specific needs.

Using viewport units (vh and vw) is a fantastic way to ensure that your layout containers and margins scale proportionally to the screen size. However, relying solely on viewport units for every single UI element can become unwieldy and difficult to maintain, especially for typography and smaller components or when trying to match the design 1:1.

If you are prototyping quickly and want an easily scalable and responsive layout, the CSS-only vh method is incredibly efficient. However, it can be somewhat vulnerable for a final production build when confronted with extreme edge cases, like super-ultrawide displays.

The Dynamic Scale Factor via JavaScript method is highly consistent and provides precise control over how your UI scales. While it does come with a slight performance trade-off whenever the window gets resized, it is worth noting that in a compiled game environment, players rarely resize their windows mid-gameplay.

Our general recommendation is to evaluate these trade-offs and find the balance that best fits your project’s technical requirements and production timeline.

Beyond automatic resolution scaling, many players - particularly those with accessibility needs - rely on a UI Scale setting to increase the size of menus and HUDs. The technique is the same root-em approach, but driven by a player preference rather than a screen resolution.

Expose a CSS custom property --scale on the root element, and calculate the root font-size from it:

base.css
:root {
--scale: 1;
font-size: calc(16px * var(--scale));
}

A scale of 1.5 enlarges the full UI by 50% - one property change, entire UI scales.

function applyUIScale(factor) {
document.documentElement.style.setProperty('--scale', factor);
}
// Called when the player adjusts the "UI Scale" slider in settings
engine.on('UIScaleChanged', (factor) => {
applyUIScale(factor);
});
// On UI load, restore saved scale preference
const savedScale = engine.call('GetSavedUIScale') ?? 1;
applyUIScale(savedScale);

Sensible default scale steps for a game are 0.75, 1.0, 1.25, 1.5, and 2.0. Avoid allowing arbitrarily large values without testing your layout at those extremes.

Instead of a float slider, named steps give you explicit control over which scale values your layout has been tested with:

:root { font-size: 14px; } /* small */
:root.scale-normal { font-size: 16px; }
:root.scale-large { font-size: 20px; }
:root.scale-xlarge { font-size: 24px; }
const SCALE_CLASSES = ['scale-small', 'scale-normal', 'scale-large', 'scale-xlarge'];
function applyUIScaleClass(level) {
SCALE_CLASSES.forEach(cls => document.documentElement.classList.remove(cls));
document.documentElement.classList.add(`scale-${level}`);
}
engine.on('UIScaleChanged', (level) => applyUIScaleClass(level));

Named steps reduce the risk of untested layout breakage compared to continuous scaling.

Testing Layout Integrity at Scale Extremes

Section titled “Testing Layout Integrity at Scale Extremes”

Changing the root font size can expose latent layout issues. Test explicitly at your defined scale extremes from the Inspector console:

document.documentElement.style.fontSize = '24px'; // Test at XL scale
document.documentElement.style.fontSize = '12px'; // Test at minimum scale

Check for: text overflow in fixed-height containers, flex row items wrapping unexpectedly, icon-to-text misalignment where the icon size was set in px but the text is in rem, and scroll areas becoming unreachable because their parent grew beyond the viewport.

Not every measurement should scale. Border widths, box shadows, and fine outlines typically look better at a fixed pixel value regardless of scale:

.menu-button {
font-size: 1rem;
padding: 0.75rem 1.5rem;
border: 1px solid rgba(255, 255, 255, 0.3); /* intentional px */
border-radius: 4px; /* intentional px */
}

Document these intentional px values with a comment so contributors don’t convert them unnecessarily.