Native UI: drawers drawn by Pulsar
Send a small JSON spec and Pulsar draws your drawer and settings page with its own controls. They open instantly, follow light and dark mode and the user’s text size, and look like the rest of Pulsar.
The shape of a spec
A SettingSpec is {sections}. An ActionSpec (your drawer) adds a required title, an optional subtitle and an optional header toggle. Each section has an optional title, its items and an optional footnote.
// ActionSpec — popup.set
{ "title": "Easy Read", "subtitle": "On for news.example",
"toggle": { "id": "site", "value": true, "title": "Easy Read on news.example" },
"sections": [ Section, … ] }
// SettingSpec — settings.set
{ "sections": [ Section, … ] }
// Section
{ "title": "Text size" | null, "items": [ Item, … ], "footnote": "…", "card": false }Every item has a type and a unique id (1–40 of A–Z a–z 0–9 . _ -). The user’s taps come back as popup.action (drawer) or settings.changed (settings page) with that id.
A whole drawer
{
"title": "Easy Read",
"subtitle": "On for news.example",
"toggle": {"id": "site", "value": true, "title": "Easy Read on news.example"},
"sections": [
{
"title": "Text size",
"items": [
{
"type": "chips",
"id": "size",
"value": "m",
"options": [
{"value": "s", "label": "Small"},
{"value": "m", "label": "Medium"},
{"value": "l", "label": "Large"}
]
}
]
},
{
"title": null,
"card": true,
"items": [
{
"type": "toggle",
"id": "spacing",
"title": "Roomier lines",
"sub": "More space between lines",
"value": true
}
]
},
{
"title": "Your week",
"items": [
{
"type": "stats",
"id": "week",
"tiles": [
{"label": "Today", "value": "8", "sub": "pages made easier"},
{"label": "This week", "bars": [4, 7, 3, 9, 6, 11, 8]}
]
}
]
},
{
"title": null,
"items": [
{
"type": "cta",
"id": "open",
"label": "See your reading week",
"primary": true
}
]
}
]
}toggle
An on/off switch. Sends true or false. enabled: false shows it faded and locked. icon and sub are optional.
{
"sections": [
{
"title": "Blocking",
"items": [
{
"type": "toggle",
"id": "ads",
"title": "Block ads",
"value": true,
"sub": "On every site",
"icon": "shield"
},
{
"type": "toggle",
"id": "popups",
"title": "Block pop-ups",
"value": false,
"icon": "warning",
"enabled": false
}
]
}
]
}choice
One of a list, shown as a row with the current choice; a tap opens the list. Use it for 4 or more options, or long labels. value must be one of the 1–20 options.
{
"sections": [
{
"title": null,
"items": [
{
"type": "choice",
"id": "level",
"title": "Strength",
"value": "std",
"icon": "shield",
"options": [
{"value": "std", "label": "Standard"},
{"value": "strict", "label": "Strict"}
]
}
]
}
]
}chips
One of 2–5 short options, all visible. Sends the option’s value. style: "cards" draws equal cards with a colour swatch (#RRGGBB) over each label.
{
"sections": [
{
"title": "Mode",
"items": [
{
"type": "chips",
"id": "mode",
"value": "std",
"options": [
{"value": "off", "label": "Off"},
{"value": "std", "label": "Standard"},
{"value": "strict", "label": "Strict"}
]
}
]
}
]
}{
"sections": [
{
"title": null,
"items": [
{
"type": "chips",
"id": "theme",
"style": "cards",
"value": "dark",
"options": [
{"value": "dark", "label": "Dark", "swatch": "#212121"},
{"value": "dim", "label": "Dim", "swatch": "#4A4A4A"},
{"value": "sepia", "label": "Sepia", "swatch": "#F3EAD7"}
]
}
]
}
]
}slider
A number between min and max (min < max, min ≤ value ≤ max), moving by step, shown with its unit. Sends the number when the finger lifts. In a card: true section it is drawn as the ink slider of Dark Mode.
{
"sections": [
{
"title": null,
"card": true,
"items": [
{
"type": "slider",
"id": "b",
"title": "Brightness",
"min": 60,
"max": 100,
"value": 90,
"step": 5,
"unit": "%"
},
{
"type": "slider",
"id": "c",
"title": "Contrast",
"min": 80,
"max": 130,
"value": 105,
"step": 5,
"unit": "%"
}
]
}
]
}text
A text field. hint is the placeholder; multiline: true makes it a box. Sends the string when the user is done. Values up to 4000 characters.
{
"sections": [
{
"title": null,
"items": [
{
"type": "text",
"id": "css",
"title": "Custom CSS",
"value": "",
"hint": "a { … }",
"multiline": true
}
]
}
]
}cta
A full-width button for the drawer’s main action. primary: true is black (white in dark mode); use it once. primary: false is the outlined button. Sends null.
{
"sections": [
{
"title": null,
"items": [
{"type": "cta", "id": "open", "label": "Open dashboard", "primary": true},
{"type": "cta", "id": "later", "label": "Not now", "primary": false}
]
}
]
}media
A picture card with a title and sub. In API v1 it is drawn as a tile in your tint (images can’t be sent yet; an image field is ignored). A tap sends null.
{
"sections": [
{
"title": "Up next",
"items": [
{
"type": "media",
"id": "v1",
"title": "Mountain Lakes",
"sub": "Blue Hour · 1080p · 43 min"
}
]
}
]
}stats
1–4 tiles. A tile shows a value with a label and sub, or bars: up to 14 numbers ≥ 0, drawn as a small bar chart with the last one in ink. Not tappable.
{
"sections": [
{
"title": null,
"items": [
{
"type": "stats",
"id": "st",
"tiles": [
{"label": "On this page", "value": "14", "sub": "ads and trackers"},
{"label": "This week", "bars": [3, 5, 2, 8, 6, 9, 14]}
]
}
]
}
]
}Header switch and card sections
toggle on the ActionSpec puts a switch at the right of the title block, for “on for this site”. It sends popup.action with its own id (different from every item’s). card: true on a section draws it as one card: sliders as ink sliders, toggles and buttons as rows.
{
"title": "Dark Mode",
"subtitle": "On for news.example",
"toggle": {"id": "site", "value": true},
"sections": [
{
"title": null,
"items": [
{
"type": "chips",
"id": "theme",
"style": "cards",
"value": "dark",
"options": [
{"value": "dark", "label": "Dark", "swatch": "#212121"},
{"value": "dim", "label": "Dim", "swatch": "#4A4A4A"},
{"value": "sepia", "label": "Sepia", "swatch": "#F3EAD7"}
]
}
]
},
{
"title": null,
"card": true,
"items": [
{
"type": "slider",
"id": "brightness",
"title": "Brightness",
"min": 60,
"max": 100,
"value": 90,
"step": 5,
"unit": "%"
}
]
},
{
"title": null,
"card": true,
"items": [
{
"type": "toggle",
"id": "follow",
"title": "Follow phone theme",
"sub": "Dark when your phone is dark",
"value": false
},
{
"type": "button",
"id": "off",
"title": "Sites where it’s off",
"value": "2"
}
]
}
]
}A settings page
The same items, sent with settings.set. Pulsar draws them in Settings › Extensions › your name with section titles, icons and footnotes.
{
"sections": [
{
"title": "Easy Read",
"items": [
{
"type": "toggle",
"id": "everywhere",
"title": "On for every site",
"sub": "Turn it off to pick sites one by one",
"value": false,
"icon": "globe"
},
{
"type": "slider",
"id": "lineHeight",
"title": "Line height",
"min": 150,
"max": 200,
"step": 10,
"unit": "%",
"value": 170
}
]
},
{
"title": null,
"items": [
{
"type": "button",
"id": "reset",
"title": "Forget my site choices",
"sub": "4 sites have their own choice",
"icon": "trash",
"danger": true
}
],
"footnote": "Easy Read never sends anything off your phone."
}
]
}More than one view
A drawer can show another view: on a button tap, send a different spec with popup.set (Dark Mode’s “Sites where it’s off” list). Add a cta like Done to go back, and go back to your first view on popup.closed.
let view = "main";
Pulsar.on("popup.action", async (m) => {
if (m.item === "offSites") view = "sites";
if (m.item === "back") view = "main";
await Pulsar.call("popup.set", view === "sites" ? sitesSpec() : mainSpec(m.host));
});
Pulsar.on("popup.closed", () => { view = "main"; });Rules and limits
Pulsar checks every spec strictly. A broken spec is refused with bad_params and a message saying what is wrong; the drawer keeps your last good spec.
| Rule | Limit |
|---|---|
| Sections | At most 12 |
| Items | At most 40 in all sections together |
| Item ids | 1–40 of A–Z a–z 0–9 . _ -, unique; the header toggle’s id differs from all |
| Texts (titles, subs, labels, footnotes) | At most 200 characters |
| A text item’s value | At most 4000 characters |
| choice / chips | 1–20 options; value must be one of them |
| slider | min < max and min ≤ value ≤ max |
| stats | 1–4 tiles; bars up to 14 numbers ≥ 0 |
| swatch, tint | #RRGGBB |
| Message size | At most 64 KB |
| Answer to popup.opening | Within 400 ms (1.5 s when Pulsar woke you) |
- Settings values: the spec’s values are defaults; Pulsar stores the user’s choices and drops values that don’t fit a new spec.
- Unknown icon names show no icon. Unknown fields are ignored, so newer fields never break older Pulsars.
- Pulsar draws your text as you send it. Write it in the user’s language (
browser.i18n).
Icons
Use these names in icon fields and in the manifest’s pulsar.icon. The same drawings are in the UI kit’s pulsar-icons.svg for your own pages; the faded ones are only in the kit.