Skip to content

Style a pop

See where your pop runs for the browser baseline and the distinction between the editor and saved-state rendering, and make it work on iPhone for compact layouts, safe areas, touch targets and haptics.

Give the app one clear visual identity: paper for an invoice or note, an instrument for a timer or mixer, or a custom skin when the silhouette helps its job. Keep the primary action obvious at the initial window size. Build depth from a backing surface and purposeful controls rather than repeated generic cards.

pop.ts’s theme declares the colors a person may change. Every value is a lowercase #rrggbb color, or #rrggbbaa when it is translucent (an opaque color leaves out ff):

theme: {
surface: "#f7f4eb",
panel: "#e3e9df",
ink: "#182126",
muted: "#687276",
accent: "#db6648",
accentSoft: "#db66482e",
},

The builder stores these defaults in the template and refuses anything that is not a palette color. The runtime applies them, with the person’s changes, before mounting the app. Each key becomes --pop-KEY, preserving case. Declare only colors worth changing: the theme panel lists every one, in the order you wrote them.

Fonts, sizes and colors derived from the palette belong in your CSS. A derived color that mixes a theme color follows the person’s change automatically. Do not maintain a second defaults stylesheet.

Import styles.css from App.svelte (or any component). Vite emits minified ui.css for the editor, including component styles and dependencies reached through CSS @import and url(). A capture’s CSS is exactly what its own Preview/Export/Icon/Widget role imports, in import order, including component CSS. Captures do not inherit the editor’s stylesheets: import shared CSS in each role or a component it imports. pop dev still mounts static views inside the editor page. Styles ship as ordinary CSS.

styles.css
:root {
--font: "Avenir Next", Avenir, sans-serif;
--rule: color-mix(in srgb, var(--pop-ink) 15%, transparent);
}
body {
color: var(--pop-ink);
background: var(--pop-surface);
font-family: var(--font);
}
.my-pop-paper {
padding: clamp(1rem, 5vw, 2.5rem);
}
.my-pop-footer {
border-top: 1px dashed var(--rule);
background: var(--pop-panel);
& > span {
font-size: 12px;
color: var(--pop-muted);
}
& button {
border: 2px solid var(--pop-ink);
background: var(--pop-accent);
}
}
@media (max-width: 360px) {
.my-pop-paper {
padding: 1rem;
}
}

Use readable app-prefixed classes in markup. Group descendants and interaction states with their base component. Nesting is supported and lowered for the host’s Safari target. Use flex/grid reflow, container queries, visible focus, and reduced-motion styles. Keep controls clear of transparent edges. For phone-sized windows, see make it work on iPhone.

Bits UI menus can render in portals. Give the portal content its own class and target attributes directly, such as .my-pop-menu [data-highlighted]; a selector below the editor root cannot reach a portal outside that root. Svelte <style> blocks remain available for component-local rules, but plain CSS is the starter convention.

The host disables ordinary text selection in every pop’s editor, including pop dev, so dragging across labels and controls feels like using an app. Inputs, textareas (including readonly fields), and contenteditable regions keep normal text selection and editing. Captures and PDF text are unaffected.

Enable selection for content people may want to copy, such as notes, addresses, or code. Add an ordinary rule to styles.css; the host default has zero specificity and needs no !important override:

/* Allow selection in a content region, including its descendants. */
.my-pop-content {
-webkit-user-select: text;
user-select: text;
}
/* Or allow selection throughout the pop, including portals. */
body {
-webkit-user-select: text;
user-select: text;
}

Choose the region rule or the whole-pop rule to suit your app. Put the region’s class on portal content too when it should be copyable. This is an authoring CSS choice, not a theme setting.

In the Mac app, the palette button in a document’s toolbar, or Window ▸ Theme, opens the theme panel beside the window. It shows a color picker and hex field for every declared color. Changes restyle the pop as you make them and save with the document like any edit. Reset to Original returns every color to the template’s. Edit ▸ Undo restores the previous colors; a color-picker drag is one undo step.

Export Theme… saves the document’s full palette as a theme file. Import Theme… replaces the palette with one exported from a document made with the same template; colors the file leaves out return to the template’s. A file made for another template, or naming a color this template doesn’t declare, is refused whole.

The CLI does the same:

Terminal window
bunx @hitslop/pop theme get My.pop
bunx @hitslop/pop theme set My.pop --values '{"accent":"#7050ad","surface":"#f5efff"}'
bunx @hitslop/pop theme reset My.pop --token accent
bunx @hitslop/pop theme reset My.pop
bunx @hitslop/pop theme export My.pop --output Purple.json
bunx @hitslop/pop theme import Other.pop Purple.json

Each read or edit prints the complete defaults, overrides, and effective palettes. Example responses for the declared palette above (only the changed tokens are shown in defaults and effective; overrides is complete):

Command Response excerpt
theme get My.pop {"defaults":{"accent":"#db6648","surface":"#f7f4eb"},"overrides":{},"effective":{"accent":"#db6648","surface":"#f7f4eb"}}
theme set My.pop … {"defaults":{"accent":"#db6648","surface":"#f7f4eb"},"overrides":{"accent":"#7050ad","surface":"#f5efff"},"effective":{"accent":"#7050ad","surface":"#f5efff"}}
theme reset My.pop --token accent {"defaults":{"accent":"#db6648","surface":"#f7f4eb"},"overrides":{"surface":"#f5efff"},"effective":{"accent":"#db6648","surface":"#f5efff"}}
theme reset My.pop The original palette from the first response, with "overrides":{}.
theme export My.pop --output Purple.json /Users/you/Documents/Purple.json (your path will differ).
theme import Other.pop Purple.json The original palette from the first response, assuming the same template.

This sequence exports after resetting. Export before resetting to keep your custom colors.

A theme file names its template and every color:

{
"template": "my-pop",
"values": {
"accent": "#7050ad",
"surface": "#f5efff"
}
}

Themes live inside the .pop file:

Part Purpose
Theme defaults Immutable, from the author’s pop.ts when the template is built.
Theme overrides The colors this writable document changed, saved with its contents.

Only declared colors can change, and a document stores only the ones that differ from the template’s. An open window applies accepted CLI changes at once; unknown colors and invalid values are refused. Fonts and layout stay authored. Registered masters and generated templates remain unchanged.

Every presentation mode receives host page resets, window sizing, and initial size variables. The automatic app root fills the window; prefer height: 100%, grid, or flex inside it; override the page sizing deliberately only when your layout needs it. Keep surface styling in your app. Transparent and PNG-skinned modes also override ordinary html/body backgrounds with transparency, so paint the visible surface inside the wrapper. Use the native toolbar handle for dragging. See manifest and windows and PNG window skins.

Preview.svelte, Icon.svelte, Export.svelte and Widget.svelte render saved document data and theme independently of the editor. All captures, including selectable vector PDF, use the native Rust renderer. Export layouts should use normal flow, without nested scrolling or fixed viewport heights. Preview.svelte and Icon.svelte are required; Export.svelte is optional. All must render synchronously; keep editor controls out of it. See icons, widgets, previews, and exports for supported styling, capture sizing and behavior.

See how .pop files work for how a theme travels with a document.