Developers

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.

pack.json
"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" }
]
OptionWhat it doesDefault
codeThe CSS or JavaScript, as text.required
whenOnly while every setting id → value holds: {"theme": "dark", "cook_mode": true}.always
run_atdocument_start, document_end or document_idle.document_idle
hostsOnly on some of the app’s hosts: ["m.example.com"].all the app’s domains
pathsGlobs over the path and query, starting with /; * is anything: ["/recipes/*"].every page
all_framestrue 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_atScripts runCSS
document_startBefore the page’s own code; the document is still empty.Added as early as Pulsar can.
document_endOnce the HTML is there (DOMContentLoaded).The same.
document_idleA little later, when the page has settled.The same.
  • run_at is 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-app class (set by a document_start script) 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.

json
{ "code": "html { font-size: 112.5% !important; }", "when": { "text_size": "l" } }
  • CSS follows a settings change at once on open pages.
  • Scripts with when start 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
__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
});
app.js — cook mode
// @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.

pack.json
"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/*"] }
]
FieldRule
actionset, remove or append.
nameA header name, up to 64 characters.
valuePlain printable text, no line breaks, up to 1024 characters. remove takes none.
hosts, paths, whenAs 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 with Sec- or Proxy-.
  • Referer may only be removed, never set. At most 20 rules.
  • Say what they do in changes, like anything else (“Mobile layout · improved”).

Limits

WhatLimit
The whole pack1 MB (a website’s pack: 512 KB, it is fetched over the network)
CSS and JS together512 KB
Code blocks40, CSS and JS together (empty ones don’t count)
Header rules20
Settings12 sections, 40 items
Share targets5, 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-scheme inside that block. Set color-scheme so form fields and scroll bars match.
  • Pick a theme_color that 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 paths to run code only where it is needed.
  • Use document_start only for code that must come first, like setting a class on <html>. The rest can wait for document_idle.
  • No timers that poll the page. Use events or a MutationObserver on a small part of it.