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
| Parameter | Type | Description |
|---|---|---|
uiScale | number | Scale 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
| Type | Description |
|---|---|
number | Current 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
| Parameter | Type | Description |
|---|---|---|
uiScale | number | Current scale multiplier |
uiAspectRatio | number | Current aspect ratio from useUiAspectRatio() |
Return Value
| Property | Type | Description |
|---|---|---|
uiWidthFactor | number | App width relative to screen long side |
uiHeightFactor | number | App height relative to screen long side |
Understanding the Factors
uiWidthFactor = 1: App width matches the long side of the displayuiWidthFactor = 0.5: App width is half the long sideuiHeightFactor = 1: App height matches the long side of the displayuiHeightFactor = 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;
}Updated 21 days ago