CSS, JS and request headers
Your pack can add CSS and JavaScript to your own pages, and change a few request headers. They run only on your domains, only inside the app.
Code blocks
css and js are lists of blocks. A plain string is a block with the defaults; an object adds options.
"css": [
".get-app-banner { display: none !important; }",
{ "code": ":root { color-scheme: dark; --site-bg: #121212; }",
"when": { "theme": "dark" },
"run_at": "document_start" },
{ "code": ".recipe-steps li { padding-block: 12px; }",
"paths": ["/recipes/*"] }
],
"js": [
{ "code": "document.documentElement.classList.add('in-pulsar-app');",
"run_at": "document_start" }
]| Option | What it does | Default |
|---|---|---|
| code | The CSS or JavaScript, as text. | required |
| when | Only while every setting id → value holds: {"theme": "dark", "cook_mode": true}. | always |
| run_at | document_start, document_end or document_idle. | document_idle |
| hosts | Only on some of the app’s hosts: ["m.example.com"]. | all the app’s domains |
| paths | Globs over the path and query, starting with /; * is anything: ["/recipes/*"]. | every page |
| all_frames | true to run in frames too (only frames on your domains). | false |
Write them as filesIn the App starter you write
app.css and app.js; each @block {…} line starts a block with its options, and node build.mjs puts them into the pack.Where code runs
- Only on pages of the app’s
domains(and their subdomains), never wider. - Only inside the app: its App-mode window or its own tab. The same site in a normal tab, a private tab or another app gets none of it.
- Each script runs in its own sandbox, like an extension’s content script: it sees the page’s DOM, not the page’s own variables.
- Nothing runs while the app is off or switched off by Pulsar.
When code runs
| run_at | Scripts run | CSS |
|---|---|---|
| document_start | Before the page’s own code; the document is still empty. | Added as early as Pulsar can. |
| document_end | Once the HTML is there (DOMContentLoaded). | The same. |
| document_idle | A little later, when the page has settled. | The same. |
run_atis honoured for scripts. CSS waits for a quick check that the page really is inside your app, so on the app’s first page it may come a moment after the first paint. Later pages in the same window get it as they start.- So don’t rely on pack CSS to hide something before the very first paint. If it must never show in the app, have your site leave it out when the
in-pulsar-appclass (set by adocument_startscript) or a request header says so. - An app with request headers reloads its first page once, so the headers apply to it too.
Conditions on settings
A block with when runs only while the user’s settings match. Every key must be a setting of the pack, and every value one that setting can take: true/false for a toggle, one of the options for choice and chips, a number in range for a slider.
{ "code": "html { font-size: 112.5% !important; }", "when": { "text_size": "l" } }- CSS follows a settings change at once on open pages.
- Scripts with
whenstart on the next page load. For a change to take effect at once, read the settings yourself and listen for changes (below).
The page API: __pulsarApp
Your scripts get one object from Pulsar:
__pulsarApp.id // your app's id, "example-kitchen"
__pulsarApp.settings() // the user's values: { theme: "auto", text_size: "m", cook_mode: true, … }
__pulsarApp.on("settings", (e) => {
// e.item: the id that changed, e.value: its new value, e.values: all values
});// @block {"run_at": "document_end", "paths": ["/recipes/*"]}
let lock = null;
async function update(on) {
if (on && !lock && document.visibilityState === "visible") {
try { lock = await navigator.wakeLock.request("screen"); lock.onrelease = () => (lock = null); } catch {}
} else if (!on && lock) { lock.release(); lock = null; }
}
update(__pulsarApp.settings().cook_mode === true);
__pulsarApp.on("settings", (e) => { if (e.item === "cook_mode") update(e.value === true); });
document.addEventListener("visibilitychange", () => update(__pulsarApp.settings().cook_mode === true));Request headers
headers changes the request headers your app sends to your own domains: to tell your server it is the app, or to drop the Referer on links out.
"headers": [
{ "action": "set", "name": "X-Example-Client", "value": "pulsar-app" },
{ "action": "set", "name": "X-Example-Text-Size", "value": "large", "when": { "text_size": "l" } },
{ "action": "remove", "name": "Referer", "paths": ["/out/*"] }
]| Field | Rule |
|---|---|
| action | set, remove or append. |
| name | A header name, up to 64 characters. |
| value | Plain printable text, no line breaks, up to 1024 characters. remove takes none. |
| hosts, paths, when | As for code blocks. |
- Request headers only. A pack can never change response headers, so it can’t remove a site’s frame or content-security rules, or set cookies.
- Only on requests to the app’s own domains, only inside the app. Requests your pages make to other sites keep their headers.
- Never:
Cookie,Cookie2,Authorization,Host,Origin,Content-Length,Connection,Keep-Alive,Transfer-Encoding,TE,Trailer,Upgrade,Expect,Accept-Encoding,Via,Forwarded,X-Forwarded-For,X-Forwarded-Host,X-Real-IP, or anything starting withSec-orProxy-. Referermay only be removed, never set. At most 20 rules.- Say what they do in
changes, like anything else (“Mobile layout · improved”).
Limits
| What | Limit |
|---|---|
| The whole pack | 1 MB (a website’s pack: 512 KB, it is fetched over the network) |
| CSS and JS together | 512 KB |
| Code blocks | 40, CSS and JS together (empty ones don’t count) |
| Header rules | 20 |
| Settings | 12 sections, 40 items |
| Share targets | 5, each with up to 20 sites |
Good practices
Light and dark
- You own the site: set your own CSS variables under a
when(:root { --site-bg: #121212 }) rather than inverting the whole page. - Offer “Like the phone” and follow
prefers-color-schemeinside that block. Setcolor-schemeso form fields and scroll bars match. - Pick a
theme_colorthat works with white text and with your site’s header.
Don’t break the site
- Use your own class names. A rule that hides
div:nth-child(3)breaks the next time the page changes. - Prefer small changes: hide your own “Get the app” banner, give buttons room, keep the rest as it is.
- Wrap scripts so an error can’t stop the page: check that an element is there before you use it.
- Test every setting, in a normal tab too: it must show none of your app’s CSS or JS.
Fast on a phone
- Keep code small; it runs on every page of the app. Use
pathsto run code only where it is needed. - Use
document_startonly for code that must come first, like setting a class on<html>. The rest can wait fordocument_idle. - No timers that poll the page. Use events or a
MutationObserveron a small part of it.