Skip to content

Build a custom Stackyard widget

A widget is one folder in ui/widgets/. This page builds one from the template in the repository.

Small
Medium
The finished widget, running with sample data.

A widget has three files. widget.json defines its settings. data.js runs on the server and calls your service. index.html runs on the dashboard and draws the result.

cp -r docs/widget-template ui/widgets/mywidget
{
"name": "mywidget",
"label": "My Widget",
"sizes": ["small", "medium"],
"fields": [
{ "key": "url", "type": "text", "label": "Service URL", "placeholder": "http://host:port" },
{ "key": "apiKey", "type": "secret", "label": "API key", "optional": true },
{ "key": "showTotal", "type": "toggle", "label": "Show total", "default": true }
]
}

name must match the folder. Each field becomes a row in the widget’s settings. A secret never returns to the browser. See Manifest.

module.exports = async function (ctx) {
const { url, apiKey } = ctx.config;
if (!url) ctx.fail('Enter the service URL.', { kind: ctx.KIND.INVALID });
const r = await ctx.fetchJSON(`${ctx.normalizeBase(url)}/api/items`, {
headers: apiKey ? { 'X-Api-Key': apiKey } : {},
timeout: 8000,
});
return {
items: (r.data.items || []).slice(0, 10).map(i => ({ name: i.name })),
total: r.data.total ?? 0,
};
};

The return value is served at /api/widget-data/<id>. The template returns { error } when the URL is missing. Use ctx.fail as shown, so the widget shows its failure state. See Data.

<script type="module">
import { poll } from '/js/widget-toolbox.js?v=1';
const root = document.getElementById('root');
poll({
render: data => { root.textContent = `${data.total} items`; },
isEmpty: data => !data.items?.length,
interval: 30000,
});
</script>

poll() fetches the data and handles loading, empty, stale and failed states. The template adds the markup and styles. See Widget page.

Restart Stackyard, then in Settings, Dashboard, press Add and pick My Widget. If it is missing, the container log says why.

Before a pull request, go through the checklist.