UI Scale Hooks

React hooks for responsive UI scaling across different screen sizes and aspect ratios in digital signage

UI Scale Hooks

Digital signage applications run on displays ranging from small tablets to massive video walls, often at different resolutions (Full HD, 4K, 8K). Standard CSS sizing approaches that work for web development create problems in this environment.

The SDK provides React hooks that solve responsive scaling for display-based applications. Import from @telemetryos/sdk/react.

The Problem with CSS Units

The default rem unit equals 16 pixels. This pixel-based definition means applications appear differently sized depending on display resolution:

  • A 16px font on a Full HD (1920x1080) TV takes up a certain percentage of screen height
  • The same 16px font on a 4K (3840x2160) TV appears half the size relative to the screen

CSS units that depend on display resolution cannot be relied on for signage where consistent, readable sizing matters.

The Solution: Viewport-Relative Rem

By redefining rem as 1% of the viewport's long dimension (1vmax), applications scale consistently across any display resolution. A 2rem font takes up the same proportion of screen space whether viewed on Full HD or 4K.

This approach also works when applications are resized to a portion of the screen, such as a bottom ticker or side panel. Using the long dimension ensures a usable minimum font size regardless of the app's aspect ratio.

:root {
  font-size: 1vmax; /* 1rem = 1% of viewport long side */
}

Why Add a UI Scale Control?

Even with viewport-relative sizing, some scenarios require additional scaling:

  • Corner placement: An app placed in a small corner of the screen needs to scale up to remain readable
  • Distant viewing: An app on a small screen viewed from far away needs larger UI elements
  • Accessibility: Users may prefer larger text and controls

The uiScale multiplier (typically 1x-3x) gives content managers control over readability for their specific deployment.

Minimum Sizing Best Practice

Title Safe Zone

Broadcast standards define a Title Safe Area where text and graphics should remain to avoid being cut off by TV bezels or overscan. The modern standard (SMPTE ST 2046-1) specifies title safe as 90% of the screen dimensions, leaving a 5% margin on each edge.

For digital signage, this translates to approximately 3rem of padding from screen edges.

Minimum Text Size

Industry guidelines recommend text be no smaller than 4% of TV height for comfortable viewing at typical distances. With viewport-relative rem, this is approximately 2rem minimum for body text.

.app {
  padding: 3rem; /* Title safe zone */
}

.card {
  padding: 2rem;
  gap: 2rem;
  font-size: 2rem; /* Minimum readable size */
}

h1 {
  font-size: 6rem; /* Headings can be larger */
}

Hook Reference

useUiScaleToSetRem

Sets the document's root font-size based on viewport size and scale factor. Call this once in your Render view to enable rem-based responsive design.

import { store } from '@telemetryos/sdk'
import { useUiScaleToSetRem } from '@telemetryos/sdk/react'
import { useUiScaleStoreState } from '../hooks/store'

function Render() {
  const [_isLoading, uiScale] = useUiScaleStoreState(store().instance)
  useUiScaleToSetRem(uiScale)

  return <div>Your content here</div>
}

How It Works

The hook sets document.documentElement.style.fontSize to calc(1vmax * ${uiScale}):

  • At uiScale = 1: 1rem = 1% of viewport long side
  • At uiScale = 2: 1rem = 2% of viewport long side (everything doubles)
  • At uiScale = 1.5: 1rem = 1.5% of viewport long side

Parameters

ParameterTypeDescription
uiScalenumberScale multiplier (typically 1-3)

useUiAspectRatio

Returns the current window aspect ratio and updates automatically on resize. Use this to detect orientation or make layout decisions.

import { useUiAspectRatio } from '@telemetryos/sdk/react'

function ResponsiveLayout() {
  const aspectRatio = useUiAspectRatio()
  const isLandscape = aspectRatio > 1
  const isPortrait = aspectRatio < 1

  return (
    <div className={isLandscape ? 'horizontal-layout' : 'vertical-layout'}>
      {/* Adapt layout based on orientation */}
    </div>
  )
}

Return Value

TypeDescription
numberCurrent aspect ratio (window.innerWidth / window.innerHeight)
  • > 1: Landscape orientation (wider than tall)
  • < 1: Portrait orientation (taller than wide)
  • = 1: Square

useUiResponsiveFactors

Calculates width and height scaling factors for responsive layouts. These factors represent how much of the screen's long dimension your app occupies after scaling.

import { useUiAspectRatio, useUiResponsiveFactors } from '@telemetryos/sdk/react'
import { useUiScaleStoreState } from '../hooks/store'

function AdaptiveContent() {
  const [_, uiScale] = useUiScaleStoreState(store().instance)
  const aspectRatio = useUiAspectRatio()
  const { uiWidthFactor, uiHeightFactor } = useUiResponsiveFactors(uiScale, aspectRatio)

  // Hide secondary content when space is limited
  const showSidebar = uiWidthFactor > 0.5

  return (
    <div>
      <main>Primary content</main>
      {showSidebar && <aside>Secondary content</aside>}
    </div>
  )
}

Parameters

ParameterTypeDescription
uiScalenumberCurrent scale multiplier
uiAspectRationumberCurrent aspect ratio from useUiAspectRatio()

Return Value

PropertyTypeDescription
uiWidthFactornumberApp width relative to screen long side
uiHeightFactornumberApp height relative to screen long side

Understanding the Factors

  • uiWidthFactor = 1: App width matches the long side of the display
  • uiWidthFactor = 0.5: App width is half the long side
  • uiHeightFactor = 1: App height matches the long side of the display
  • uiHeightFactor = 0.5: App height is half the long side

Use these values to conditionally hide elements when space becomes too constrained.

Complete Example

Store Hook Definition

// hooks/store.ts
import { createUseStoreState } from '@telemetryos/sdk/react'

export const useUiScaleStoreState = createUseStoreState<number>('ui-scale', 1)

Settings View

// views/Settings.tsx
import { store } from '@telemetryos/sdk'
import {
  SettingsContainer,
  SettingsField,
  SettingsLabel,
  SettingsSliderFrame,
} from '@telemetryos/sdk/react'
import { useUiScaleStoreState } from '../hooks/store'

export function Settings() {
  const [isLoading, uiScale, setUiScale] = useUiScaleStoreState(store().instance)

  return (
    <SettingsContainer>
      <SettingsField>
        <SettingsLabel>UI Scale</SettingsLabel>
        <SettingsSliderFrame>
          <input
            type="range"
            min={1}
            max={3}
            step={0.01}
            disabled={isLoading}
            value={uiScale}
            onChange={(e) => setUiScale(parseFloat(e.target.value))}
          />
          <span>{uiScale}x</span>
        </SettingsSliderFrame>
      </SettingsField>
    </SettingsContainer>
  )
}

Render View

// views/Render.tsx
import { store } from '@telemetryos/sdk'
import { useUiScaleToSetRem, useUiAspectRatio, useUiResponsiveFactors } from '@telemetryos/sdk/react'
import { useUiScaleStoreState } from '../hooks/store'

export function Render() {
  const [isLoading, uiScale] = useUiScaleStoreState(store().instance)
  const aspectRatio = useUiAspectRatio()
  const { uiWidthFactor, uiHeightFactor } = useUiResponsiveFactors(uiScale, aspectRatio)

  // Apply viewport-relative scaling
  useUiScaleToSetRem(uiScale)

  if (isLoading) return null

  // Hide elements when space is constrained
  const showDetails = uiHeightFactor > 0.4

  return (
    <div className="app">
      <h1>Welcome</h1>
      {showDetails && <p>Additional details shown when there's room</p>}
    </div>
  )
}

CSS

/* styles.css */
.app {
  padding: 3rem; /* Title safe zone */
  font-size: 2rem; /* Minimum readable size */
}

h1 {
  font-size: 6rem;
  margin-bottom: 2rem;
}