WidgetBox-specific patterns and standards for building Homey dashboard widgets. Covers settings conventions, height strategies, init patterns, shared CSS, and code structure used across all WidgetBox apps. Use when: creating new widgets, modifying existing widgets, adding settings, handling height/sizing, styling with shared CSS, or standardizing code patterns.
Standards and patterns for all WidgetBox Homey dashboard widgets. This skill extends the homey skill with project-specific conventions.
Prerequisites: Read the
homeyskill first for general Homey widget development.
| App | ID | Widgets |
|---|---|---|
| Clocks | com.nielsvanbrakel.widgetbox-clocks | analog-clock, digital-clock, binary-clock, flip-clock, date, word-clock-grid, word-clock-sentence |
| Buienradar | com.nielsvanbrakel.widgetbox-buienradar | buienradar, buienradar-map, buientabel |
| Windy | com.nielsvanbrakel.widgetbox-windy | windy |
| Utilities |
com.nielsvanbrakel.widgetbox-utilities |
| stopwatch, timer |
| Layout | com.nielsvanbrakel.widgetbox-layout | spacer |
| YouTube | com.nielsvanbrakel.widgetbox-youtube | youtube |
We use Turbo to publish all apps interactively.
Ensure Versions Match: All apps should share the same version number (e.g. 0.1.0) across package.json, app.json, and .homeycompose/app.json.
Run Publish Command:
turbo run homey:publish --concurrency 1
--concurrency 1 is required to run the interactive prompts sequentially.interactive: true is set in turbo.json to enable TTY.Handle Prompts:
? Do you want to update your app's version number?To pass validation (homey app validate --level publish), every app MUST have:
assets/images/small.png (250x175)assets/images/large.png (500x350)assets/icon.svg (Source for icons)Generation: Use the icon.svg to generate the PNGs if missing.
sips -s format png icon.svg --out small.png
sips -Z 96 small.png # Or standard app icon size
# Resize mainly for store assets:
sips -z 175 250 small.png
sips -z 350 500 large.png
Most widgets support a size dropdown with these standard values:
{
"id": "size",
"type": "dropdown",
"label": { "en": "Size", "nl": "Grootte" },
"value": "medium",
"values": [
{ "id": "xsmall", "label": { "en": "Extra Small", "nl": "Extra Klein" } },
{ "id": "small", "label": { "en": "Small", "nl": "Klein" } },
{ "id": "medium", "label": { "en": "Medium", "nl": "Gemiddeld" } },
{ "id": "large", "label": { "en": "Large", "nl": "Groot" } },
{ "id": "xlarge", "label": { "en": "Extra Large", "nl": "Extra Groot" } }
]
}
Used by: All clock widgets, date widget.
Color dropdowns use Homey's built-in color palette:
{
"id": "color",
"type": "dropdown",
"label": { "en": "Color", "nl": "Kleur" },
"value": "default",
"values": [
{ "id": "default", "label": { "en": "Default", "nl": "Standaard" } },
{ "id": "blue", "label": { "en": "Blue", "nl": "Blauw" } },
{ "id": "green", "label": { "en": "Green", "nl": "Groen" } },
{ "id": "orange", "label": { "en": "Orange", "nl": "Oranje" } },
{ "id": "red", "label": { "en": "Red", "nl": "Rood" } },
{ "id": "purple", "label": { "en": "Purple", "nl": "Paars" } }
]
}
Map "default" to var(--homey-text-color) and named colors to var(--homey-color-{name}-500).
{
"id": "horizontalAlignment",
"type": "dropdown",
"label": { "en": "Horizontal Alignment", "nl": "Horizontale Uitlijning" },
"value": "center",
"values": [
{ "id": "left", "label": { "en": "Left", "nl": "Links" } },
{ "id": "center", "label": { "en": "Center", "nl": "Midden" } },
{ "id": "right", "label": { "en": "Right", "nl": "Rechts" } }
]
}
{
"id": "clockFormat",
"type": "dropdown",
"label": { "en": "Time Format", "nl": "Tijdformaat" },
"value": "24",
"values": [
{ "id": "24", "label": { "en": "24-hour", "nl": "24-uur" } },
{ "id": "12", "label": { "en": "12-hour", "nl": "12-uur" } }
]
}
{
"id": "aspectRatio",
"type": "dropdown",
"label": { "en": "Aspect Ratio", "nl": "Beeldverhouding" },
"value": "16:9",
"values": [
{ "id": "1:1", "label": { "en": "Square (1:1)" } },
{ "id": "4:3", "label": { "en": "4:3" } },
{ "id": "16:9", "label": { "en": "16:9 (Default)" } },
{ "id": "9:16", "label": { "en": "Portrait (9:16)" } },
{ "id": "21:9", "label": { "en": "Ultrawide (21:9)" } },
{ "id": "3:1", "label": { "en": "Panoramic (3:1)" } }
]
}
Use the hint property to add explanation text to settings that may not be immediately clear to the user. Always provide bilingual hints (en + nl). Use hints for:
{
"id": "lat",
"type": "text",
"label": { "en": "Latitude", "nl": "Breedtegraad" },
"hint": {
"en": "Enter the latitude of your location (e.g. 52.1326)",
"nl": "Voer de breedtegraad van je locatie in (bijv. 52.1326)"
},
"value": "52.1326"
}
Rule: Always add a
hinttotextandnumbersettings. Fordropdownandcheckboxsettings, only add a hint if the label alone doesn't sufficiently explain what the setting does.
Widgets use one of three height patterns:
Calculates height from DOM content. Used by all clock and date widgets.
function calculateTotalHeight() {
const widget = document.getElementById('widget');
return widget ? widget.offsetHeight : 128;
}
Homey.ready({ height: calculateTotalHeight() });
new ResizeObserver(() => Homey.setHeight?.(calculateTotalHeight())).observe(document.body);
Calculates height as a percentage for iframe-based widgets. Used by youtube, windy, buientabel.
function getAspectRatioPercentage(aspectRatio) {
const ratios = {
'1:1': '100%',
'4:3': '75%',
'16:9': '56.25%',
'9:16': '177.78%',
'21:9': '42.86%',
'3:1': '33.33%'
};
return ratios[aspectRatio] || '56.25%';
}
Homey.ready({ height: getAspectRatioPercentage(settings.aspectRatio || '16:9') });
Calculates from component count. Used by stopwatch, timer.
const calcHeight = () => {
const itemCount = items.length;
const itemHeight = 60;
const headerHeight = 40;
return headerHeight + (itemCount * itemHeight) + padding;
};
Homey.ready({ height: calcHeight() });
All widgets follow this initialization flow:
let currentSettings = {};
function onHomeyReady(Homey) {
currentSettings = Homey.getSettings() || {};
renderWidget();
Homey.on('settings.set', (key, value) => {
currentSettings[key] = value;
renderWidget();
Homey.setHeight?.(calculateTotalHeight());
});
// Start intervals (clocks: 1000ms, data: configurable)
Homey.ready({ height: calculateTotalHeight() });
}
Variant: Stopwatch/timer use
window.onHomeyReady = async (homey) => {}, others usefunction onHomeyReady(Homey) {}. Both work.
Clock and utility widgets import shared styles:
<link rel="stylesheet" href="../../_shared/shared-styles.css">
Located at widgets/_shared/shared-styles.css, providing:
| Class | Purpose |
|---|---|
.widget-container | Flex column, centered, standard padding |
.widget-container--compact | Reduced padding variant |
.widget-row / .widget-column | Flex row/column layouts |
.widget-center | Centered flex container |
.widget-button | Standard button with hover/active states |
.widget-button--primary | Blue primary button |
.widget-button--small | Compact button |
.widget-text-display | Large bold text (numbers) |
.widget-text-title | Medium bold text |
.widget-text-body | Default body text |
.widget-text-secondary | Secondary/muted text |
.widget-text-small | Small caption text |
.widget-text-mono | Monospace font |
.widget-loading | Loading spinner |
.widget-error | Error message |
.widget-empty | Empty state |
.widget-card | Card background with shadow |
.widget-fade-in | Fade-in animation |
.widget-pulse | Pulse animation |
.widget-sr-only | Screen reader only |
Always use var(--homey-*) variables for colors, fonts, and spacing.
| Widget Type | transparent | Rationale |
|---|---|---|
| Clock widgets | false | Card background for readability |
| Stopwatch, Timer | false | Card background for readability |
| Spacer | true | Invisible spacing element, blends with dashboard |
| Embed widgets (buienradar, windy, youtube) | not set | Iframe handles its own background |
Map color setting IDs to CSS variables:
function getColor(colorId) {
if (colorId === 'default') return 'var(--homey-text-color)';
if (colorId === 'white') return '#fff';
if (colorId === 'black') return '#000';
return `var(--homey-color-${colorId}-500)`;
}
All runtime text in widgets must use Homey.__() with keys defined in locales/en.json and locales/nl.json.
Keys live under widgets.<widgetId>.<key>:
{
"widgets": {
"buientabel": {
"loading": "Loading...",
"noRain": "No rain expected",
"error": "Something went wrong"
},
"stopwatch": {
"addStopwatch": "Add Stopwatch",
"lap": "Lap"
}
}
}
const __ = (key) => Homey.__(`widgets.my-widget.${key}`) ?? key;
en.json and nl.json are required in every app's locales/ directory| Trigger | What to update |
|---|---|
| New widget added | App's README.txt, monorepo README.md apps table, this skill's Apps Overview table |
| Widget removed | App's README.txt, monorepo README.md apps table, this skill's Apps Overview table |
| Major feature change to a widget | App's README.txt (update feature description) |
| New app added to monorepo | New README.txt, monorepo README.md, this skill's Apps Overview table |
| App removed from monorepo | Remove README.txt, update monorepo README.md, this skill's Apps Overview table |
README.txt starts with the shared WidgetBox intro paragraph (see below)Every app's README.txt must start with this exact paragraph:
WidgetBox adds clean, native-looking widgets to your Homey dashboard. Designed to fit perfectly with Homey's style, these widgets help you customize your dashboard just the way you like it.
After the intro, add a blank line and then the app-specific description.
The description field in .homeycompose/app.json is a catchy tagline shown below the app name on the store. Rules:
en and nl translationsWhen creating a new WidgetBox widget:
widgets/<id>/widget.compose.json + public/index.html../../_shared/shared-styles.csshint to all text and number settings (bilingual)locales/en.json and locales/nl.json for all runtime texttransparent based on widget type (see table above)ResizeObserver if height depends on contentpreview-dark.png and preview-light.png (1024x1024)README.txt, monorepo README.md, and this skill's Apps Overview tableThe sandbox app (apps/sandbox/) is a Vite/React application for previewing and testing widgets locally with e2e tests via Playwright.
apps/sandbox/
├── scripts/
│ └── generate-registry.js # Scans widget.compose.json files, outputs src/registry.json
├── src/
│ ├── components/
│ │ ├── Icons.jsx # SVG icon components
│ │ ├── Sidebar.jsx # Widget list grouped by app
│ │ ├── Toolbar.jsx # Theme toggle, width presets, reload
│ │ ├── WidgetPreview.jsx # Iframe preview with Homey card framing
│ │ └── SettingsPanel.jsx # Settings controls + debug scenarios
│ ├── lib/
│ │ ├── MockHomey.js # Mock Homey API (settings, height, translations, events)
│ │ ├── homeyStyles.js # Injects Homey CSS variables into iframe
│ │ ├── scenarios.js # Debug scenario definitions per widget
│ │ └── mocks/
│ │ └── buienradarMocks.js # Buienradar-specific mock data + real fetch
│ ├── App.jsx # Root component (state + composition only)
│ ├── index.css # All styles (no inline styles in components)
│ ├── main.jsx # React entry point
│ └── registry.json # Generated (gitignored), do NOT commit
├── index.html
├── package.json
└── vite.config.js
To add mock data for a new widget:
src/lib/mocks/<widgetName>Mocks.js with a handler functionMockHomey.api() methodsrc/lib/scenarios.jsindex.cssIcons.jsxMockHomey.js — delegate to mocks/ modulesregistry.json is generated — never edit manually, never commitAll widgets are tested via Playwright e2e tests against the sandbox.
tests/
├── e2e/ # Test specs per app/feature
│ ├── widgets.spec.ts # Sandbox loading tests
│ ├── clocks.spec.ts
│ ├── utilities.spec.ts
│ ├── windy.spec.ts
│ ├── youtube.spec.ts
│ ├── buienradar.spec.ts
│ ├── layout.spec.ts
│ └── sandbox-translations.spec.ts
├── pages/ # Page Object Model
│ ├── SandboxPage.ts # Base page (goto, selectWidget, settings helpers)
│ ├── ClocksPage.ts
│ ├── UtilitiesPage.ts
│ ├── WindyPage.ts
│ ├── YouTubePage.ts
│ ├── BuienradarPage.ts
│ └── LayoutPage.ts
# Run all tests
pnpm test:e2e
# Run specific test file
npx playwright test tests/e2e/clocks.spec.ts
SandboxPage for widget-specific page objectsget flipClock())SandboxPage helpers for common interactions:
selectWidget(name) — clicks widget in sidebarsetSettingCheckbox(label, checked) — toggles checkbox settingsetSettingSelect(label, option) — selects dropdown optionsetSettingInput(label, value) — fills text/number input@playwright/test (avoid unused imports)