Your first widget
A widget is a folder with a widget.json and a web page. This walks through creating one, changing it and looking at the result. You need Umbra installed and a text editor; no build tools.
1. Scaffold it
Section titled “1. Scaffold it”umbractl new my-widgetThis writes a working widget into %APPDATA%\Umbra\widgets\my-widget and shows it on your desktop straight away. (Use --name "My widget" for a display name, or --dir <folder> to scaffold somewhere else; widgets outside the community folder aren’t picked up until you load them with umbractl create <folder>.)
You now have:
my-widget/ widget.json the manifest index.html the page style.css styles main.js the code2. Read the manifest
Section titled “2. Read the manifest”{ "$schema": "https://raw.githubusercontent.com/AmineChr54/umbra/main/schemas/widget.schema.json", "id": "my-widget", "name": "my-widget", "description": "Describe in one sentence why people want this widget.", "author": "your-github-name", "version": "0.1.0", "entry": "index.html", "width": 260, "height": 120, "permissions": { "hosts": [] }, "options": { "greeting": { "label": "Greeting", "type": "text", "default": "Hello" } }}id must equal the folder name. width and height are the widget’s size in logical pixels. permissions.hosts is empty, so this widget can’t reach the internet. options becomes a settings form on the widget’s card in My widgets. The manifest reference covers every field.
3. Read the page
Section titled “3. Read the page”index.html loads Umbra’s stylesheet and SDK from the virtual host umbra.local, then your files:
<link rel="stylesheet" href="https://umbra.local/shared/umbra.css"><link rel="stylesheet" href="style.css">...<script src="https://umbra.local/shared/umbra.js"></script><script src="main.js"></script>umbra.css gives you design tokens (colors, fonts, radii), and umbra.js gives you the global umbra object: options, refresh, data fetching, saved data and more. See Page and lifecycle.
4. Change it
Section titled “4. Change it”Edit main.js or style.css, then tell Umbra to re-scan:
umbractl reloadChange the option live without editing any file:
umbractl set my-widget greeting "Good morning"5. Fetch real data
Section titled “5. Fetch real data”To call an API, declare its host in widget.json, then use umbra.fetchJson inside umbra.every:
"permissions": { "hosts": ["api.open-meteo.com"] }umbra.every(async () => { const data = await umbra.fetchJson("https://api.open-meteo.com/v1/forecast?latitude=52.52&longitude=13.41¤t=temperature_2m"); document.getElementById("app").textContent = `${data.current.temperature_2m}°`; umbra.rendered();});Requests to any host you didn’t declare are blocked, and fetchJson rejects. See the JavaScript API.
6. Look at it
Section titled “6. Look at it”umbractl capture my-widget shot.pngThis saves a PNG of just that widget and nothing else on screen. Check it at other sizes with umbractl configure my-widget --scale 2. Testing with capture has the full loop.
7. Make it good, then publish
Section titled “7. Make it good, then publish”Follow the design guidelines (true black background, tokens instead of hard-coded colors, tabular numbers), add a preview.png, and publish it by pull request so it shows up in everyone’s Browse page.
