Homey widget development for dashboard apps. Triggers on: Homey CLI, widgets, widget settings, widget styling, widget debugging, homey app create, homey app run, homey app publish, dashboard widgets, Homey App SDK. Use when user: creates widgets, configures widget settings, styles widgets with Homey CSS, debugs widgets on Android/iOS, publishes to Homey App Store, or works with Homey apps.
Build custom dashboard widgets for Homey smart home platform. Widgets are webviews (HTML/CSS/JS) displayed on user dashboards with access to the Homey API.
Compatibility: Widgets require Homey Pro with SDK
>=12.3.0. Not available on Homey Cloud.
pnpm add -g homeyCreate app?
├─ New app from scratch → homey app create
├─ Add widget to existing app → homey app widget create
├─ Run app on Homey → homey app run
├─ Install without terminal → homey app install
└─ Publish to App Store → homey app publish
Widget settings?
├─ Text input → type: "text"
├─ Multi-line text → type: "textarea"
├─ Number input → type: "number" (with optional min/max)
├─ Selection dropdown → type: "dropdown"
├─ Toggle option → type: "checkbox"
└─ Search with suggestions → type: "autocomplete"
Styling?
├─ Text styling → Use .homey-text-* classes
├─ Colors → Use --homey-color-* variables
├─ Light/dark mode → Automatic, or force with .homey-dark-mode
├─ Spacing → Use --homey-space-* units
└─ Icons → Use .homey-icon class with custom SVG
# Create new Homey app (interactive)
homey app create
# Run app on Homey (dev mode with hot reload for public/ files)
homey app run
# Install app without keeping terminal open
homey app install
# Validate app before publishing
homey app validate
# Publish to Homey App Store
homey app publish
# Create a new widget (run from app directory)
homey app widget create
# Login to Athom account
homey login
# Logout
homey logout
# Select different Homey device
homey select
# View all commands
homey --help
homey app --help
When you run homey app widget create, it creates:
widgets/<widgetId>/
├── widget.compose.json # Widget definition and settings
├── public/
│ └── index.html # Widget entry point (and other assets)
├── api.js # Backend API implementation
├── preview-dark.png # Preview image for dark mode (1024x1024)
└── preview-light.png # Preview image for light mode (1024x1024)
{
"name": { "en": "My Widget" },
"settings": [
{
"id": "my-setting",
"type": "dropdown",
"title": { "en": "Select Option" },
"value": "option1",
"values": [
{ "id": "option1", "title": { "en": "Option 1" } },
{ "id": "option2", "title": { "en": "Option 2" } }
]
}
],
"height": 200,
"transparent": false,
"api": {
"getData": { "method": "GET", "path": "/" },
"setData": { "method": "POST", "path": "/" }
}
}
Key Properties:
height: Initial height in pixels, or percentage (e.g., "100%" = square)transparent: Set true for transparent backgroundapi: Define endpoints accessible via Homey.apideprecated: Set true to hide from widget picker (existing instances still work)| Type | Value | Description |
|---|---|---|
text | string | null | Single line text, optional pattern for regex validation |
textarea | string | null | Multi-line text |
number | number | null | Numeric input, optional min/max |
dropdown | string | null | Select from predefined values array |
checkbox | boolean | null | Toggle true/false |
autocomplete | object | null | Search with suggestions |
In your index.html, use the global Homey object:
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="https://apps-sdk.developer.homey.app/css/homey.widgets.css">
<script src="https://apps-sdk.developer.homey.app/js/homey.widgets.js"></script>
</head>
<body>
<div id="content"></div>
<script>
async function init() {
// Get user settings
const settings = await Homey.getSettings();
// Call your backend API
const data = await Homey.api('GET', '/');
// Listen for app events
Homey.on('update', (data) => {
console.log('Received update:', data);
});
// Set dynamic height
Homey.setHeight(250);
// Translation
const text = Homey.__('settings.title');
// Signal widget is ready (removes loading state)
Homey.ready();
}
init();
</script>
</body>
</html>
| Method | Description |
|---|---|
Homey.ready({ height?: number }) | Signal widget is ready, optionally set height |
Homey.api(method, path, body?) | Call widget API endpoints |
Homey.on(event, callback) | Listen for app-emitted events |
Homey.__(key, tokens?) | Translate string from /locales/*.json |
Homey.getWidgetInstanceId() | Get unique instance ID |
Homey.getSettings() | Get user-configured settings |
Homey.setHeight(height) | Change widget height at runtime |
Homey.popup(url) | Open in-app browser |
Homey.hapticFeedback() | Trigger haptic feedback (call after touch event) |
Always include the Homey CSS:
<link rel="stylesheet" href="https://apps-sdk.developer.homey.app/css/homey.widgets.css">
.homey-text-bold /* Titles, important text */
.homey-text-medium /* Subtitles, emphasis */
.homey-text-regular /* Default body text */
.homey-text-small /* Small standalone text */
.homey-text-small-light /* Small text next to other text */
/* Font sizes */
--homey-font-size-xxlarge /* 32px - numbers only */
--homey-font-size-xlarge /* 24px - short phrases */
--homey-font-size-large /* 20px - numbers only */
--homey-font-size-default /* 17px - most text */
--homey-font-size-small /* 14px - captions */
/* Line heights (match with font size) */
--homey-line-height-xxlarge /* 40px */
--homey-line-height-xlarge /* 32px */
--homey-line-height-large /* 28px */
--homey-line-height-default /* 24px */
--homey-line-height-small /* 20px */
/* Font weights */
--homey-font-weight-bold /* Titles */
--homey-font-weight-medium /* Emphasis */
--homey-font-weight-regular /* Default */
/* Semantic colors */
--homey-text-color
--homey-background-color
--homey-color-highlight
--homey-color-success
--homey-color-warning
--homey-color-danger
/* Grayscale (000=white to 1000=black in light mode) */
--homey-color-mono-000 to --homey-color-mono-1000
/* Accent colors (050-900) */
--homey-color-blue-500
--homey-color-green-500
--homey-color-orange-500
--homey-color-red-500
/* Force dark mode */
.homey-dark-mode
/* Check if dark mode (in CSS) */
.homey-dark-mode .my-element { ... }
--homey-space-10-5 /* 0.5 units */
--homey-space-11 /* 1 unit */
--homey-space-11-5 /* 1.5 units */
--homey-space-12 /* 2 units */
/* etc. */
/* Widget padding */
--homey-widget-padding
'use strict';
module.exports = {
async getData({ homey, params, query, body }) {
// Access Homey instance
const devices = await homey.devices.getDevices();
// Return data to widget
return { devices: Object.keys(devices) };
},
async setData({ homey, params, query, body }) {
// Handle POST data
homey.log('Received:', body);
return { success: true };
}
};
# Run with hot reload for public/ folder
homey app run
A refresh button appears to reload index.html without full restart.
chrome://inspect in Chrome| Asset | Size | Format |
|---|---|---|
| App icon | 1024x1024 | PNG (transparent bg) |
| App image small | 250x175 | JPG/PNG |
| App image large | 500x350 | JPG/PNG |
| App image xlarge | 1000x700 | JPG/PNG |
| Widget preview | 1024x1024 | PNG (transparent bg) |
# Debug level (development)
homey app validate --level debug
# Publish level (Homey Pro)
homey app validate --level publish
# Verified level (Homey Cloud)
homey app validate --level verified
homey app validate --level publishhomey app publish
turbo with concurrency 1).The README.txt file is a plain-text story displayed on the App Store page below the app name and description. It describes what the app does in a friendly, non-technical way.
Format rules:
Description field (app.json):
description field is a short, catchy tagline shown below the app nameen and nl translationsHomey Compose splits the app manifest into modular files that get merged into the root app.json during pre-processing.
.homeycompose/app.json — The source manifest with base app metadata (id, name, description, compatibility, etc.)widgets/<id>/widget.compose.json — Individual widget definitionsapp.json — The generated output, merged from the above filesIMPORTANT: Both
.homeycompose/app.jsonAND rootapp.jsonmust exist. The CLI reads.homeycompose/app.jsonas the source and writes the merged result (with widgets, drivers, etc.) to rootapp.json. Deleting rootapp.jsoncauses errors.
WARNING: If
.homeycompose/app.jsonis missing but rootapp.jsonexists, the CLI shows:Warning: Could not find a Homey Compose app.json manifest!Always create.homeycompose/app.jsonwith the base metadata.
my-app/
├── .homeycompose/
│ └── app.json # Source manifest (base metadata only, no widgets)
├── app.json # Generated (copy of .homeycompose/app.json + merged widgets)
├── app.js # App entry point
├── package.json
├── widgets/
│ └── my-widget/
│ ├── widget.compose.json
│ └── public/
│ └── index.html
└── locales/
└── en.json
{
"id": "com.example.myapp",
"version": "1.0.0",
"compatibility": ">=12.3.0",
"sdk": 3,
"platforms": ["local"],
"name": { "en": "My App" },
"description": { "en": "App description" },
"category": ["tools"],
"brandColor": "#00B8FF",
"permissions": [],
"images": {
"small": "/assets/images/small.png",
"large": "/assets/images/large.png",
"xlarge": "/assets/images/xlarge.png"
},
"author": { "name": "Your Name", "email": "[email protected]" }
}
Key rules:
"compatibility": ">=12.3.0" (widgets need SDK 12.3+)"platforms": ["local"] (widgets are not available on Homey Cloud)"widgets" in .homeycompose/app.json — they are auto-merged from widget.compose.json filesHomey uses per-language JSON files in locales/ for runtime translations.
locales/
├── en.json # English (required)
└── nl.json # Dutch (or any other language)
{"en": {"title": "..."}})widgets.<widgetId>.<key>{
"widgets": {
"my-widget": {
"loading": "Loading...",
"error": "Something went wrong"
}
}
}
// Direct call
const text = Homey.__('widgets.my-widget.loading');
// Helper pattern (recommended)
const __ = (key) => Homey.__(`widgets.my-widget.${key}`) ?? key;
const text = __('loading');
IMPORTANT: If a translation key is missing,
Homey.__()returns the key path as a string. Always populate bothen.jsonandnl.jsonwith all keys used in widget code.
function updateHeight() {
const height = document.body.scrollHeight;
Homey.setHeight(height);
}
// Call after content changes
updateHeight();
async function refresh() {
const data = await Homey.api('GET', '/');
renderData(data);
}
// Initial load
refresh();
// Refresh every 5 minutes
setInterval(refresh, 5 * 60 * 1000);
Homey.on('settings.set', (key, value) => {
settings[key] = value;
renderWidget();
});
When building multiple Homey apps in a monorepo:
my-monorepo/
├── apps/
│ ├── com.example.app-one/
│ │ ├── .homeycompose/
│ │ │ └── app.json # Source manifest
│ │ ├── app.json # Generated manifest
│ │ ├── app.js
│ │ ├── package.json
│ │ └── widgets/
│ │ └── my-widget/
│ └── com.example.app-two/
│ └── ...
├── pnpm-workspace.yaml
└── turbo.json
Each Homey app is a standalone package that can be developed and published independently.
Add these scripts to each app's package.json:
{
"scripts": {
"homey:run": "echo 'Only one app can run in dev mode at a time. Run directly: homey app run'",
"homey:install": "homey app install",
"homey:build": "homey app build",
"homey:publish": "homey app publish"
}
}
Add matching tasks to turbo.json:
{
"tasks": {
"homey:run": { "cache": false, "persistent": true },
"homey:install": { "cache": false },
"homey:build": {},
"homey:publish": { "cache": false }
}
}
Usage:
turbo run homey:install # Install all apps to Homey
turbo run homey:build # Build all apps
turbo run homey:publish # Publish all apps
WARNING:
homey app run(dev mode) uses port 9229 for debugging. Only ONE app can run in dev mode at a time. Running multiple apps simultaneously causes a port conflict. Usehomey:installto deploy all apps, andhomey app rundirectly for single-app debugging.