Skip to main content

Style System

The Hosanna UI Style System provides a comprehensive and centralized approach to managing the visual appearance and interaction states of UI components across the application. This documentation covers the core concepts, configuration, and usage of the style system.

The Config File (style.config.json)

Hosanna apps are configured by a JSON file (commonly style.config.json) which can be bundled or served remotely. This config drives both the visual theme and behavioral settings at runtime and can be fetched dynamically to reconfigure a Roku app without code changes.

What it contains:

  • Theme tokens: colors and fonts
  • View styles (stateful): per component/view with normal, focused, selected, disabled, etc.
  • Flags and fields for behavior and feature toggles
  • Server endpoints and other metadata
  • Translations (and the active locale section)
  • CollectionView row/cell fragment styles and data maps

Example structure excerpt:

{
"$schema": "./hosanna-style-schema.json",
"note": "This file is an example of a hosanna configuration file",
"theme": {
"colors": { /* design tokens */ },
"fonts": { /* font keys mapped to font files and sizes */ }
},
"controls": {
"Button": {
"default": {
"normal": { "color": "~theme.colors.white", ... },
"focused": { /* state overrides */ },
"selected": { /* state overrides */ }
},
"primary": { /* variant */ }
}
}
}
Tip: Prefer Tokens & Keys

Reference theme tokens via ~theme.colors.* and ~theme.fonts.* instead of hardcoding values. Use styleKey like controls.Button.default to keep styles consistent and reusable.

Collection row/cell styles are also declared and merged at runtime:

public resolveRowStyleForRow(row: ICollectionViewDataSourceRow): IOperationResult {
let style = this.getStyle<ICollectionViewRowSettings>(row.settingsKey ?? '');
if (row.settingsOverrides) style = this.deepMerge(style, row.settingsOverrides);
(row as JsonData).settings = style;
}

Loading and using styles (StyleRegistry API)

The StyleRegistry holds the config and resolves styles, references, and stateful dictionaries.

export interface IStyleRegistry {
setStyleJson(json: { [key: string]: IStyle }): void;
addStyle(key: string, style: IStyle, merge?: boolean): void;
getStyle<T = IStyle>(key: string, isNested?: boolean, seenStyles?: Record<string, boolean>): T;
getTranslation(key: string): string;
setLanguage(languageCode: string, sectionKey?: string): void;
resetCachedStyles(): void;
configure(): void;
}
  • Initialize once at app start: styleRegistry.configure() then styleRegistry.setStyleJson(...) (from bundled JSON or server response).
  • Retrieve styles anywhere by key: getStyle('controls.Button.default').
  • Add or extend at runtime: addStyle('controls.Button.primary', {...}, true) merges deeply.
Extending Styles

Use $extends to inherit from a base key, then override only what you need. References starting with ~ let you pull values from other keys.

References and extension

StyleRegistry resolves ~ references and $extends for composing styles, plus dynamic ${...} bindings for data maps.

// $extends merges from a base key, then resolves nested values and references
if ((style as IStyleDictionary).$extends) {
const baseKey = (style as JsonData).$extends as string;
const baseStyle = this.getStyle(baseKey, true, seenStyles);
const merged = this.deepMerge(baseStyle as JsonData, resolvedStyle);
delete merged.$extends;
return this.resolveNestedStyles(merged, seenStyles, ownerId);
}
// ~reference pulls values from other style keys
if (firstCharacter === '~') {
const nestedKey = (value as string).slice(1);
const nestedStyle = this.getStyle(nestedKey, true, seenStyles);
return typeof nestedStyle === 'object' ? (nestedStyle as JsonData)[key] : nestedStyle;
}
// ${data.*} binds to a data map for fragments; language/resolution helpers supported
if (value.startsWith('${')) { /* map to activeDataMap.view/data/fn */ }

Stateful styles

Components can have state dictionaries for visual states:

export interface IStatefulStyle {
none?: IStyleDictionary;
normal: IStyleDictionary;
focused?: IStyleDictionary;
selected?: IStyleDictionary;
error?: IStyleDictionary;
disabled?: IStyleDictionary;
focuseeSelected?: IStyleDictionary;
}

Views automatically apply the active state dictionary and can change it on focus/blur or programmatically.

Caution: Don’t Mutate During Animation

Avoid setting view state inside animation ticks. Let animations drive renderer props; persist final logical state on completion.

Translations and language switching

Config can contain translations and a locale section. You can switch language at runtime:

public setLanguage(languageCode: string, sectionKey = 'locale'): void {
this.styles[sectionKey] = (this.styles.translations as IStyleDictionary)[languageCode];
this.resetCachedStyles();
}

Use getTranslation(key) or reference via ~ keys in styles.

Inline translation in views

Views can resolve translations via translate('{{path.to.key}}') with optional pipe-separated parameters.

protected translate(key: string): string {
if (!key.includes('{{') || !key.includes('}}')) return key;
const inner = key.slice(2, -2);
const pipeIndex = inner.indexOf('|');
const rawKey = pipeIndex !== -1 ? inner.slice(0, pipeIndex) : inner;
const argsRaw = pipeIndex !== -1 ? inner.slice(pipeIndex + 1) : '';
const args = argsRaw.length > 0 ? argsRaw.split('|') : [];
const translation = this.styleRegistry.getTranslation(rawKey);
let result = translation;
for (let i = 0; i < args.length; i++) {
result = result.replace('${' + (i + 1).toString() + '}', args[i]);
}
return result;
}

Examples (from app code):

Label({ text: this.translate('{{locale.settings.autoplay}}') })
Label({ text: this.translate('{{locale.settings.language}}') })
EWTNButton({ text: this.translate('{{locale.settings.logout}}') })
Tip: Keep Keys Flat and Predictable

Prefer short, kebab-cased keys (locale.settings.language) and avoid deep nesting beyond what’s necessary for clarity.

Parameter substitution:

// locale.example.greeting = "Hello ${1}, you have ${2} messages"
this.translate('{{locale.example.greeting|George|5}}');
// -> "Hello George, you have 5 messages"

Date/locale formatting

For date/time formatting, use HsDate and set its locale:

// pseudo usage (HsDate API)
HsDate.setLocale('fr-FR');
// Then format dates according to the active locale

VSCode extension support

The Hosanna VSCode extension provides style key navigation and completions:

  • Jump to style definitions referenced by ~ keys
  • Completion for style keys and theme tokens
  • Inline hover info for resolved styles

Dynamic configuration

Because the config is plain JSON, you can host it on a server and load it at app launch to dynamically configure the app (theme, behaviors, endpoints, row/cell fragments, translations). After fetching, call styleRegistry.setStyleJson(remoteJson) and optionally setLanguage(...).

Basic Style Example

import { view } from '../hosanna-ui/lib/decorators';
import { ViewState, ViewStruct } from '../hosanna-ui/views/lib/view-api';
import { Rectangle } from '../hosanna-ui/views/primitives/Rectangle';
import { HGroup } from '../hosanna-ui/views/groups/HGroup';
import { VGroup } from '../hosanna-ui/views/groups/VGroup';
import { BaseExampleScreenView } from './BaseExampleScreen';

@view('StyleExample')
export class StyleExampleView extends BaseExampleScreenView<StyleExampleState> {
protected getViews(): ViewStruct<ViewState>[] {
return [
VGroup([
// Example of styled rectangles with theme colors
HGroup([
Rectangle({
width: 200,
height: 100,
color: '~theme.colors.red', // Using theme color
focused: {
scale: 1.1, // Scale up on focus
color: '~theme.colors.grey1' // Change color on focus
}
}),
Rectangle({
width: 200,
height: 100,
color: '~theme.colors.grey2',
selected: {
borderColor: '~theme.colors.white',
borderWidth: 2
}
})
]).itemSpacing(20),

// Example of text with theme typography
Label({
text: "Styled Text",
font: '~theme.fonts.heading-32',
color: '~theme.colors.white'
})
]).padding(20)
];
}
}

Theme Configuration

The theme configuration defines our global design tokens:

const theme = {
colors: {
black: "#000000",
white: "#FFFFFF",
red: "#EA1D2D",
grey1: "#121B24",
grey2: "#2A2323A",
grey3: "#232D38"
},
fonts: {
"heading-56": "Ag-heading, 56",
"heading-32": "Ag-heading, 32",
"body1": "Ag-regular, 32",
"body2": "Ag-regular, 28"
}
};

Component States Example

@view('StateExample')
export class StateExampleView extends BaseExampleScreenView<StateExampleState> {
protected getViews(): ViewStruct<ViewState>[] {
return [
Button({
// Normal state
normal: {
backgroundColor: '~theme.colors.grey1',
textColor: '~theme.colors.white',
font: '~theme.fonts.body1'
},
// Focused state
focused: {
backgroundColor: '~theme.colors.red',
scale: 1.1
},
// Selected state
selected: {
backgroundColor: '~theme.colors.grey3',
borderColor: '~theme.colors.white',
borderWidth: 2
},
// Disabled state
disabled: {
backgroundColor: '~theme.colors.grey2',
opacity: 0.5
}
})
];
}
}

Layout and Spacing

@view('LayoutExample')
export class LayoutExampleView extends BaseExampleScreenView<LayoutExampleState> {
protected getViews(): ViewStruct<ViewState>[] {
return [
VGroup([
// Header section
HGroup([
Label({
text: "Title",
font: '~theme.fonts.heading-56',
color: '~theme.colors.white'
}),
Spacer({ width: 20 }),
Icon({ color: '~theme.colors.red' })
]).itemSpacing(16).padding(20),

// Content section
VGroup([
Rectangle({
width: '100%',
height: 200,
color: '~theme.colors.grey1'
}),
HGroup([
// Content items with consistent spacing
...items.map(item => ContentItem({
backgroundColor: '~theme.colors.grey2',
margin: { top: 8, bottom: 8 }
}))
]).itemSpacing(12)
]).padding({ left: 24, right: 24 })
])
];
}
}

Best Practices

  1. Use Theme Tokens: Always reference theme tokens using the ~theme syntax instead of hardcoding values
  2. State Management: Define styles for all relevant component states
  3. Responsive Design: Consider different device types and screen sizes when defining styles
  4. Consistency: Maintain consistent spacing and sizing using the predefined system values
  5. Accessibility: Ensure sufficient contrast ratios and appropriate focus indicators