Qaid
ARTICLE

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.

Qaid Team

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 against positive but 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. Without container, you get a modal — and zIndex, modalWidth, and backdropOpacity only apply in modal mode.
  • Use colors.positive for the dominant accent (progress bar, primary button), colors.marker for selection highlights, and colors.negative for validation errors. These map to the stable --qaid-positive, --qaid-marker, and --qaid-negative shadow-root variables.
  • fontFamily and fontSize write to --qaid-font-family and --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 css string is injected straight into the shadow root. The CSS variables are the stable API; internal class selectors like .qaid-input and .qaid-card are not — verify them in devtools and treat them as a private surface.
  • Multiple-choice options accept an image field; pair it with imageAlignment: "vertical" for a poster-grid feel or "horizontal" for a list feel.
  • currency, range, and date cover budget, urgency, and start-date inputs natively — set min on date to today’s ISO string to block past dates.
  • autoAdvance: true is a snappiness flag for single-choice and range questions; it doesn’t apply to multi-select, text, currency, or date.
  • Set autoFocus: false in CMS previews so the embed doesn’t steal focus from the editor.
Back to all articles