Theming & Inline Embeds: A Renovation Estimate Form
Render the quests embed inline, match it to your brand with custom colors and CSS, and pick layouts that feel native to your site — using a contractor's estimate form as the running example.
Picture a general contractor’s marketing site. Above the fold, a hero image of a finished kitchen. Below it, a single call to action: Get your renovation estimate. When a prospect clicks, you don’t want a modal to slam down over the page — you want the form to feel like part of the landing page itself, in the contractor’s brand colors, with the contractor’s typography. A native, on-page experience.
That’s what the inline mode of @qaiddev/quests-embed is for. This guide walks through every design-oriented config option using that contractor’s estimate form as the running example: project type, budget, urgency, desired start date, and a notes field.
Inline vs Modal
The quests embed has two render modes, controlled by a single config option.
- No
container— the embed renders as a modal overlay, centered on the page with a backdrop. Good for trigger-on-click flows or full-page interruptions. container: "#some-selector"— the embed renders inline inside the matching element. No backdrop, no overlay, no z-index stacking. Just a card that lives in your layout’s flow.
For the contractor’s landing page, you want inline. Drop a section into the page where the form should live:
<section id="estimate-form" style="max-width: 640px; margin: 0 auto;"></section>
Then point the embed at it:
new QaidQuests({
endpoint: "/api/responses",
apiKey: "qd_pk_live_...",
container: "#estimate-form",
questionnaire: { /* ... */ }
});
That’s the whole switch. A few config options become no-ops in inline mode:
zIndex— only used to layer the modal over your page; ignored when the embed lives inside a container you already positioned.modalWidth— replaced by the natural width of your container element. If your container is 640px wide, the form is 640px wide.backdropOpacity— there’s no backdrop in inline mode.
If you set those options anyway, nothing breaks. They just don’t do anything. Style the container instead — max-width, margin, padding — using your normal page CSS.
Brand Colors
The contractor’s brand palette is a deep forest green, a warm gold accent, and a muted clay red for errors. Drop those straight into colors:
new QaidQuests({
endpoint: "/api/responses",
apiKey: "qd_pk_live_...",
container: "#estimate-form",
colors: {
positive: "#2D5A3D",
negative: "#B43E2C",
marker: "#D4A24C"
},
questionnaire: { /* ... */ }
});
Here’s where each color shows up in the rendered embed:
positive— the progress bar fill, the primary Next/Submit button background, and the focus ring on text/currency inputs. This is your dominant accent. Pick the color you’d use for a primary button on the rest of your site.marker— the highlight on the currently selected option in a multiple-choice question, and the active thumb on a range slider. This is a secondary accent — pick something that reads againstpositivebut still belongs to the same family.negative— required-field validation messages and destructive states. Reserve a color you only want users to see when something has gone wrong.
Under the hood, these three values are written into the shadow root as CSS variables:
--qaid-positive: #2D5A3D;
--qaid-negative: #B43E2C;
--qaid-marker: #D4A24C;
Setting colors in config is the easy path. Targeting --qaid-positive directly inside your css string is the escape hatch — useful if you want different shades in different states (a darker green on hover, for instance) by writing CSS that builds on the variable.
Both hex (#2D5A3D) and rgb() / rgba() work. Use whichever your design system already uses.
Typography
Default font stack is system-ui, -apple-system, sans-serif at 16px. That’s a fine baseline, but a contractor with a brand book is going to want something specific. Most likely a humanist sans like Manrope, Inter, or DM Sans.
Load the font on the host page (the embed itself does not fetch fonts — it just references whatever’s loaded in the parent document):
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link
href="https://fonts.googleapis.com/css2?family=Manrope:wght@400;500;700&display=swap"
rel="stylesheet"
>
Then pass the family into the embed config:
new QaidQuests({
endpoint: "/api/responses",
apiKey: "qd_pk_live_...",
container: "#estimate-form",
fontFamily: "'Manrope', system-ui, sans-serif",
fontSize: 16,
questionnaire: { /* ... */ }
});
Whatever you pass to fontFamily lands directly in the --qaid-font-family CSS variable inside the shadow root, so any valid CSS font-family stack is fair game — fallbacks, generic families, all of it.
fontSize is a base size in pixels, exposed as --qaid-font-size. Bumping it to 17 or 18 makes the form feel more generous on a desktop landing page; dropping to 14 makes it dense and businesslike. Match the size of the body copy on the rest of your page and the embed will blend in.
Progress Position
Multi-step forms show a step counter (Step 2 of 5) and a progress bar. By default these sit at the top of the card, above the question. That’s the right choice for a tall form-card on a roomy desktop layout.
For the contractor’s landing page, where the form is competing with a hero image and a value prop block above the fold, you want the question to sit as high as possible — make the very first thing the prospect sees be “What kind of project are you planning?” not a thin gray progress bar.
progressPosition: "bottom"
That moves the counter and progress bar into the footer, next to the Next button. The question label is now the first text in the card.
Quick rule of thumb:
"top"(default) — when the form is the main thing on the page. Tall card, plenty of vertical space, you want to set expectations up front."bottom"— when the form is one of several things on the page and you’re fighting for vertical space. Compact, above-the-fold, question-first.
Custom CSS via the css Option
colors, fontFamily, fontSize, modalWidth, and backdropOpacity cover the common-case theming. For everything else, use css — a string that gets injected directly into the shadow root. Style anything you can reach.
For the contractor’s form, two practical overrides: rounder inputs with a brand-colored focus ring, and a denser card padding so the form fits cleanly between the hero and the next page section.
new QaidQuests({
endpoint: "/api/responses",
apiKey: "qd_pk_live_...",
container: "#estimate-form",
colors: {
positive: "#2D5A3D",
negative: "#B43E2C",
marker: "#D4A24C"
},
css: `
.qaid-input, .qaid-textarea {
border-radius: 12px;
}
.qaid-input:focus, .qaid-textarea:focus {
outline: 3px solid rgba(212, 162, 76, 0.35);
outline-offset: 2px;
}
.qaid-card { padding: 28px; }
`,
questionnaire: { /* ... */ }
});
Important caveat about internal class names. The exact class names used inside the rendered embed (.qaid-input, .qaid-textarea, .qaid-card, etc.) are not part of the public API. They can change between versions of @qaiddev/quests-embed without a major bump. If you write CSS that depends on them, treat it like a private API — verify the selectors yourself by inspecting the shadow DOM in devtools, and re-check after upgrades.
The stable surface, the one we promise to keep working, is the CSS variable layer:
--qaid-positive--qaid-negative--qaid-marker--qaid-modal-width--qaid-backdrop-opacity--qaid-font-family--qaid-font-size
Anything you can express in terms of those variables — color, type, dimensions — should go through them, either via the typed config options or by referencing var(--qaid-positive) from your css string. Reach for raw class selectors only when the variables can’t get you there: border-radius, padding, layout tweaks, animations, decorative elements.
Image-Driven Multiple Choice
The first question on the contractor’s form is “What kind of project are you planning?” — kitchen, bathroom, addition, or outdoor. Pure text would be functional. Adding a photo to each option is what turns the form into a brand experience.
The quests embed supports an image field on every multiple-choice option. Combined with imageAlignment, you can render the choices either as a vertical gallery (image above label) or a horizontal list (image left of label).
{
"id": "project_type",
"type": "multiple-choice",
"label": "What kind of project are you planning?",
"imageAlignment": "vertical",
"options": [
{ "value": "kitchen", "label": "Kitchen remodel", "image": "/img/kitchen.jpg" },
{ "value": "bath", "label": "Bathroom remodel", "image": "/img/bath.jpg" },
{ "value": "addition", "label": "Home addition", "image": "/img/addition.jpg" },
{ "value": "outdoor", "label": "Outdoor / deck", "image": "/img/outdoor.jpg" }
]
}
Picking the alignment:
"vertical"— images sit above the label, centered. This produces a gallery feel: each option is a poster card. Good for a small number of visually distinct choices (4-6) where the image is the message."horizontal"— images sit to the left of the label, with the label and description to the right. This produces a list feel. Good for longer lists (6-12) where the image is supporting context but the label is doing the heavy lifting.
For a contractor with strong project photography, "vertical" is almost always the right call. The before/after kitchen shot does more selling than any label ever will.
The images render as 1:1 thumbnails. Crop your source images square, ideally at 600x600 or larger so they look sharp on retina screens, and serve them as WebP for fast first-load.
Numeric and Date Inputs in Context
After project type, the form needs three quantitative answers. The quests embed has a dedicated question type for each.
Budget — currency. A USD input with min/max guardrails and a placeholder so the prospect knows the expected range:
{
"id": "budget",
"type": "currency",
"label": "What's your budget?",
"description": "A rough range is fine — we'll refine during the consultation.",
"currency": "USD",
"min": 5000,
"max": 500000,
"placeholder": "Estimated budget",
"required": true
}
The input formats the number with locale-aware thousands separators as the user types. min and max are enforced — a prospect typing 1000 will be told the project is below your minimum, which is a useful filter.
Urgency — range. A 1-to-10 slider with a unit suffix and a midpoint default:
{
"id": "urgency",
"type": "range",
"label": "How urgent is this project?",
"description": "1 = exploring ideas, 10 = need to start immediately.",
"min": 1,
"max": 10,
"step": 1,
"defaultValue": 5,
"unit": "/10"
}
The unit string is a suffix shown next to the live value, so the slider reads 5/10, 7/10, 10/10. Sliders are great for soft, intuitive answers like urgency where a number is faster than a sentence.
Desired start — date. Block past dates by setting min to today’s ISO date:
{
"id": "start_date",
"type": "date",
"label": "When would you like to start?",
"min": "2026-04-27"
}
Setting min is the difference between a usable date picker and one where someone can submit a request to start construction last March. If you generate the form server-side, set min to today’s date dynamically. If the form is fully static, refresh min whenever you redeploy.
autoAdvance for Snappy Single-Choice
By default, every question shows a Next button — the prospect clicks an option, then clicks Next. For single-select questions and ranges, that’s an extra step that doesn’t add anything.
autoAdvance: true
With autoAdvance on, the form advances automatically as soon as the prospect selects an option in a single-choice multiple-choice question, or releases the thumb on a range slider. It’s noticeably snappier — especially with the image-grid project type, where a click on the kitchen photo takes the prospect straight to the budget question without a manual Next.
A few question types ignore autoAdvance, because there’s no clear moment when the answer is “done”:
- multi-select multiple-choice (
multiple: true) — the prospect might want to pick more than one option, so the form waits for an explicit Next. - text — the prospect could still be typing.
- currency — same reasoning.
- date — date pickers vary too much across browsers; auto-advancing on partial input is hostile.
So autoAdvance: true is functionally a “single-choice and slider snappiness” flag. The other input types behave the same regardless.
autoFocus: false for CMS Previews
By default, the embed auto-focuses the first input on each step. That’s the right behavior on a live landing page — the prospect wants to start typing immediately.
It’s the wrong behavior inside a page builder. If a contractor’s marketing person is editing the landing page in a CMS that renders a live preview iframe, every render will yank focus from the editor’s text field into the embed’s input. Annoying for the editor, hostile for content workflows.
autoFocus: false
Set this in CMS preview contexts only. Most setups detect the preview mode (a query param, an iframe parent check) and pass autoFocus: false only when previewing — the production page keeps autoFocus: true (the default).
Putting It Together
The full config block for the contractor’s landing-page estimate form, with every option from this guide combined:
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link
href="https://fonts.googleapis.com/css2?family=Manrope:wght@400;500;700&display=swap"
rel="stylesheet"
>
<section id="estimate-form" style="max-width: 640px; margin: 0 auto;"></section>
<script type="module">
import { QaidQuests } from "@qaiddev/quests-embed";
new QaidQuests({
endpoint: "/api/responses",
apiKey: "qd_pk_live_...",
container: "#estimate-form",
colors: {
positive: "#2D5A3D",
negative: "#B43E2C",
marker: "#D4A24C"
},
fontFamily: "'Manrope', system-ui, sans-serif",
fontSize: 16,
progressPosition: "bottom",
autoAdvance: true,
css: `
.qaid-input, .qaid-textarea {
border-radius: 12px;
}
.qaid-input:focus, .qaid-textarea:focus {
outline: 3px solid rgba(212, 162, 76, 0.35);
outline-offset: 2px;
}
.qaid-card { padding: 28px; }
`,
questionnaire: {
title: "Get your renovation estimate",
description: "A few quick questions and we'll be in touch within one business day.",
submitLabel: "Request estimate",
thankYouTitle: "Thanks — we'll be in touch.",
thankYouMessage: "A project manager will reach out within one business day.",
questions: [
{
id: "project_type",
type: "multiple-choice",
label: "What kind of project are you planning?",
imageAlignment: "vertical",
required: true,
options: [
{ value: "kitchen", label: "Kitchen remodel", image: "/img/kitchen.jpg" },
{ value: "bath", label: "Bathroom remodel", image: "/img/bath.jpg" },
{ value: "addition", label: "Home addition", image: "/img/addition.jpg" },
{ value: "outdoor", label: "Outdoor / deck", image: "/img/outdoor.jpg" }
]
},
{
id: "budget",
type: "currency",
label: "What's your budget?",
description: "A rough range is fine — we'll refine during the consultation.",
currency: "USD",
min: 5000,
max: 500000,
placeholder: "Estimated budget",
required: true
},
{
id: "urgency",
type: "range",
label: "How urgent is this project?",
description: "1 = exploring ideas, 10 = need to start immediately.",
min: 1,
max: 10,
step: 1,
defaultValue: 5,
unit: "/10"
},
{
id: "start_date",
type: "date",
label: "When would you like to start?",
min: "2026-04-27"
},
{
id: "notes",
type: "text",
label: "Anything else we should know?",
description: "Materials, must-haves, constraints — anything helps.",
multiline: true,
maxLength: 1000
}
]
}
});
</script>
That’s a complete, brand-matched, inline estimate form. Drop it into a landing page section and it reads as part of the page, not a widget bolted on top.
For a CMS preview build, the only change is adding autoFocus: false to the config. Everything else stays identical.
Summary
- Set
container: "#selector"to render the embed inline inside an element you already laid out. Withoutcontainer, you get a modal — andzIndex,modalWidth, andbackdropOpacityonly apply in modal mode. - Use
colors.positivefor the dominant accent (progress bar, primary button),colors.markerfor selection highlights, andcolors.negativefor validation errors. These map to the stable--qaid-positive,--qaid-marker, and--qaid-negativeshadow-root variables. fontFamilyandfontSizewrite to--qaid-font-familyand--qaid-font-size. Load custom fonts on the host page; the embed picks them up.progressPosition: "bottom"keeps the question at the top of the card — better for compact, above-the-fold layouts.- The
cssstring is injected straight into the shadow root. The CSS variables are the stable API; internal class selectors like.qaid-inputand.qaid-cardare not — verify them in devtools and treat them as a private surface. - Multiple-choice options accept an
imagefield; pair it withimageAlignment: "vertical"for a poster-grid feel or"horizontal"for a list feel. currency,range, anddatecover budget, urgency, and start-date inputs natively — setminondateto today’s ISO string to block past dates.autoAdvance: trueis a snappiness flag for single-choice and range questions; it doesn’t apply to multi-select, text, currency, or date.- Set
autoFocus: falsein CMS previews so the embed doesn’t steal focus from the editor.