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:
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()
thenstyleRegistry.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.
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.
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}}') })
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
- Use Theme Tokens: Always reference theme tokens using the
~theme
syntax instead of hardcoding values - State Management: Define styles for all relevant component states
- Responsive Design: Consider different device types and screen sizes when defining styles
- Consistency: Maintain consistent spacing and sizing using the predefined system values
- Accessibility: Ensure sufficient contrast ratios and appropriate focus indicators