Documentation

Widget Creation Guide

A Widgey widget is a folder containing an HTML file and a manifest. No SDK, no build step, no compiler. If you can write a web page you already have what you need. This guide covers the rest.

Quick start

The fastest route is inside the app: Settings → Layout → + Create scaffolds a working widget from a template, in the right folder, ready to edit. To start from scratch:

  1. Create a folder named for your widget in %APPDATA%\WidgetDashboard\UserWidgets\, named reverse-DNS style like yourname.server-ping.
  2. Add manifest.json. You do not need an id field, because the folder name is the ID.
  3. Add widget.html with your markup and script.
  4. Screenshot it and save that as preview.png.
  5. Open Widgey settings and your widget is in the catalog.
Edits hot-reload. Widgey watches the UserWidgets folder, so saving widget.html updates the running widget without restarting the app. If your widget doesn't appear at all, the Layout tab tells you exactly which rule it failed.

File structure

yourname.server-ping/
manifest.json    Required. Metadata and configuration.
widget.html      Required. Your UI and logic.
preview.png      Required to publish. A screenshot of it running.
(anything else)  Optional. CSS, JS, images, fonts, audio, video.

Your folder name is your widget ID

Letters, numbers, dots, hyphens and underscores; 100 characters maximum. It can't start or end with . or -, and can't contain ... If you do include an id field in the manifest it must match the folder name exactly, or the widget won't load.

Prefixing with your own name avoids a practical collision: if a user downloads two widgets as plain folders and both are called clock, the second overwrites the first.

The ID is not a security identity. Widgets installed from the Workshop are keyed on the file ID Steam assigns, which no publisher gets to pick. Two Workshop widgets sharing an id stay completely separate: different catalog entries, different settings, different browser storage.

Manifest reference

Every field manifest.json accepts:

FieldTypeRequiredDefaultDescription
idstringNofolder nameOmit it, the folder name is used
namestringYesn/aDisplay name, max 100 chars
descriptionstringYesn/aShort description, max 500 chars
versionstringYesn/aSemantic version: major.minor.patch
authorstringYesn/aAuthor name, max 50 chars
glyphstringYesn/aA Segoe MDL2 Assets icon character
categorystringYesn/aAudio, Finance, Fun, Games, News, Sports, System, Time, Utilities or Weather
accentColorstringNo#3DA5FFHex colour for the card border and glyph
defaultColSpanintNo2Default width in grid columns (1–8)
defaultRowSpanintNo2Default height in grid rows (1–8)
minColSpanintNo1Minimum width when resized
maxColSpanintNo4Maximum width when resized
minRowSpanintNo1Minimum height when resized
maxRowSpanintNo4Maximum height when resized
permissionsstring[]No[]See Permissions
networkHostsstring[]With network[]Hosts this widget may reach, max 20
hasSettingsboolNofalseShow a gear icon in the widget header
transparentboolNofalseTransparent container background
manifest.json
{
  "name": "Server Ping",
  "description": "Round-trip latency to my game servers.",
  "version": "1.0.0",
  "author": "Your Name",
  "glyph": "",
  "category": "Utilities",
  "accentColor": "#39E28B",
  "defaultColSpan": 2,
  "defaultRowSpan": 2,
  "hasSettings": true,
  "permissions": ["network"],
  "networkHosts": ["api.example.com"]
}
There is no resizable field. A widget is resizable when maxColSpan is greater than minColSpan, or maxRowSpan is greater than minRowSpan, on either axis. To make one a fixed size, set min, max and default to the same value on both axes:
{
  // A 2x2 widget the user cannot resize
  "minColSpan": 2, "maxColSpan": 2, "defaultColSpan": 2,
  "minRowSpan": 2, "maxRowSpan": 2, "defaultRowSpan": 2
}

Worth doing when a layout only works at one size. A fixed-size widget shows no resize grip, so nobody can drag it into a shape it can't render. The playground has a Resizable toggle that writes these six fields for you.

Validation runs on every load, not just at publish. A widget that fails does not appear in the catalog, and the Layout tab lists the reason. A widget that passes on your machine will pass on a subscriber's too.

Writing the widget

Rules that matter

  • Set body to height: 100vh and overflow: hidden, because the widget is sized by the grid, not by its content.
  • Use background: transparent on body and paint your own container, so the user's theme and transparency settings still work.
  • Add user-select: none unless your widget is genuinely about selecting text.
  • Everything is local: no CDN scripts, no remote fonts, unless the host is on your networkHosts list.
widget.html
<!DOCTYPE html>
<html>
<head>
<style>
  * { margin: 0; padding: 0; box-sizing: border-box; }
  body {
    background: transparent;
    font-family: 'Segoe UI', sans-serif;
    color: #F2F2F5;
    height: 100vh;
    overflow: hidden;
    user-select: none;
  }
  .container {
    background: #0E0A18;
    height: 100%;
    border-radius: 0 0 12px 12px;
    padding: 16px;
  }
</style>
</head>
<body>
  <div class="container">
    <!-- your widget -->
  </div>
  <script>
    // your logic
  </script>
</body>
</html>

Bridge API

Widgey injects a widgey global into your page before any of your scripts run. There is no plumbing to write and nothing to import.

the whole API
// Settings. All async, all return promises.
const name = await widgey.settings.get('userName');   // string or null
widgey.settings.set('userName', 'Alice');             // fire and forget
const all  = await widgey.settings.getAll();          // { key: value }
widgey.settings.delete('userName');

// Hardware sensors. Requires the "system" permission.
const cpu = await widgey.system.get('cpu_temp');      // "52"
const every = await widgey.system.getAll();

// Audio spectrum. Requires the "audio" permission.
const bands = await widgey.audio.getBands();         // int[64], 0–100

// Theme changes
widgey.onTheme(theme => {
  document.documentElement.style.setProperty('--accent', theme.accentColor);
});
Every call resolves. An unknown sensor key, a stopped sensor service, or a missing setting all resolve to null rather than hanging, so you never need a timeout of your own.

Testing in a normal browser

The widgey global only exists inside the host, so guard for it if you also open the page in Chrome while developing:

const hasBridge = typeof widgey !== 'undefined';
const temp = hasBridge
  ? await widgey.system.get('cpu_temp')
  : '42';  // stub value for browser testing

Hardware sensors

Requires the system permission. All values come back as integer strings, or null if the sensor service isn't running.

KeyUnitDescription
cpu_temp°CCPU package temperature
gpu_temp°CGPU core temperature
gpu_hotspot_temp°CGPU hotspot temperature
cpu_load%CPU total utilization
gpu_load%GPU core utilization
gpu_mem_load%GPU memory utilization
ram_used / ram_totalGBRAM in use, total RAM
ram_load%RAM utilization
vram_used_mb / vram_total_mbMBGPU VRAM in use, total
cpu_clock / gpu_clockMHzClock speeds
cpu_power / gpu_powerWPower draw
gpu_fanRPMGPU fan speed (0 = fans stopped)
mobo_temp°CMotherboard temperature
storage_temp°CStorage device temperature
net_up_kbps / net_down_kbpsKbpsNetwork throughput
// One bridge call instead of twenty
const s = await widgey.system.getAll();
// { cpu_temp: "52", gpu_temp: "41", cpu_load: "23", ... }

Audio spectrum

Requires the audio permission. getBands() returns 64 magnitudes from 0–100, mapped logarithmically from low to high frequency, and all zeros when nothing is playing.

setInterval(async () => {
  const bands = await widgey.audio.getBands();
  drawVisualizer(bands);
}, 33);  // ~30fps is smooth; faster buys you nothing

Permissions

Permissions are declared in the manifest and shown to users before they install. Ask for less and more people will install your widget.

PermissionGrantsTypical use
systemRead hardware sensor dataMonitors, temperature gauges, performance dashboards
audioRead the 64-band audio spectrumVisualizers, music-reactive widgets
networkOutbound requests to declared hosts onlyWeather, stocks, any API-driven widget

The network permission

network on its own does nothing. Pair it with the hosts you actually use:

{
  "permissions": ["network"],
  "networkHosts": ["api.open-meteo.com", "*.githubusercontent.com"]
}
  • Exact host: api.example.com matches only that host.
  • Subdomain wildcard: *.example.com matches a.example.com and a.b.example.com, but not example.com itself, so list the apex separately if you need it. A leading *. is the only wildcard allowed, and *.com is rejected as too broad.
  • HTTPS is required for everything except loopback (localhost, 127.0.0.1, ::1), which may use plain HTTP for local companion apps.
  • No schemes, paths or ports. https://api.example.com/v1 and api.example.com:8443 are both rejected.
  • Maximum 20 entries.
Anything not on the list gets a 403, including images, fonts and scripts. That covers fetch, XMLHttpRequest, EventSource and ordinary resource loads. WebSockets are checked against the same list, and RTCPeerConnection throws for every widget.
Bundle your assets and you may not need network at all. file:/// inside your own folder, plus data: and blob: URIs, always work and need no permission. A widget that declares no permissions earns the Sandboxed badge and appears under the "Sandboxed only" filter.

Theme integration

Widgets are told when the user changes theme. Honouring it costs a few lines and is the difference between a widget that belongs on the dashboard and one that's visibly bolted on.

// Called once on load and again on every theme change
widgey.onTheme(theme => {
  const root = document.documentElement.style;
  root.setProperty('--accent', theme.accentColor);
  root.setProperty('--radius', theme.cornerRadius + 'px');
});

Publishing to the Workshop

Publishing happens inside Widgey. There is no zip to upload and no website account to create.

  1. Put your widget folder in %APPDATA%\WidgetDashboard\UserWidgets\.
  2. Add a preview.png, at least 200×200.
  3. Open Settings → Layout, select your widget, click Publish to Workshop.

Widgey validates the widget, shows exactly what will be published, including the permissions subscribers will see, then uploads it. The Workshop file ID is written back into your manifest.json, so the button becomes Update on Workshop from then on.

Preview images

  • Minimum 200×200. Anything larger is fine.
  • Widgey pads it to a square and scales it to 512×512 for the Workshop thumbnail. It never crops, so the store shows the same picture the dashboard does. Your own preview.png is left untouched.
  • Show the widget actually running, not a logo. A dark background matches the dashboard.

Updating

Bump version in the manifest and click Update on Workshop. Steam pushes it to every subscriber automatically. There is no update badge to chase.

Permission changes are visible. Your permission set is re-published on every update, so if v2 adds network, subscribers see that on the item's page. Adding permissions in an update is never silent.
First publish only: Steam requires you to accept the Workshop legal agreement before your item is visible to anyone else. Widgey spots this and opens the item page so you can accept. Until you do, only you can see it.

Troubleshooting

The widget doesn't appear in the catalog

It failed validation. The Layout tab lists rejected widgets and the reason for each, which is usually a missing required manifest field, an id that doesn't match the folder name, or an invalid category.

The widget shows blank

Almost always a JavaScript error before anything renders, or content sized outside a height: 100vh body. Open the page in a normal browser with a stubbed widgey object and check the console.

Sensor calls return null

Either the widget is missing the system permission, or the sensor service isn't installed and running. Check Settings → Hardware Monitoring. Unknown sensor keys also return null rather than throwing, so check your spelling against the table above.

Network requests fail with 403

The host isn't on your networkHosts list. Remember that *.example.com does not cover example.com itself, and that every subresource — images, fonts, scripts — is checked, not just fetch.

Settings don't persist

settings.set() is fire-and-forget; if you write and immediately read back in the same tick you may race it. Use await on a get when you need the round trip.

Ready to build one?

Widgey scaffolds a working widget for you. Settings → Layout → + Create.