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. A contractor's estimate form is the running example.

Qaid Team

A general contractor’s site: hero shot of a finished kitchen, one call to action underneath. A modal slamming over that page undoes the work the page was doing.

Inline mode is the alternative. Every design option in @qaiddev/quests-embed appears below, building one estimate form: project type, budget, urgency, start date, notes.

Inline or modal

One option decides. With no container, the embed is a modal over a backdrop, which suits a click-to-open flow. With container: "#some-selector", it renders inside that element: no backdrop, no overlay, no stacking order to think about.

Put a section where the form belongs:

<section id="estimate-form" style="max-width: 640px; margin: 0 auto;"></section>

And point at it:

new QaidQuests({
  endpoint: "/api/responses",
  apiKey: "qd_pk_live_...",
  container: "#estimate-form",
  questionnaire: { /* ... */ }
});

Three options stop meaning anything then. zIndex only layers a modal, modalWidth gives way to your container’s own width, and backdropOpacity has no backdrop. Setting them breaks nothing and does nothing. Size the container with your own CSS.

Brand colours

Forest green, a gold accent, a clay red for errors:

new QaidQuests({
  endpoint: "/api/responses",
  apiKey: "qd_pk_live_...",
  container: "#estimate-form",
  colors: {
    positive: "#2D5A3D",
    negative: "#B43E2C",
    marker:   "#D4A24C"
  },
  questionnaire: { /* ... */ }
});

positive is the dominant one: progress bar, the Next and Submit button, the focus ring on inputs. Use whatever your site uses for a primary button. marker highlights the selected option and the slider thumb, so it wants to read against positive while staying in the family. negative is validation and destructive states, seen only when something went wrong.

All three land in the shadow root as CSS variables:

--qaid-positive: #2D5A3D;
--qaid-negative: #B43E2C;
--qaid-marker:   #D4A24C;

Config is the easy path; referencing --qaid-positive from your css string is the escape hatch, and it is how you get a darker green on hover without hard-coding a second value. Hex and rgb() both work.

Typography

The default is system-ui, -apple-system, sans-serif at 16px, which is a baseline and not a brand. The embed fetches no fonts of its own; it uses what the parent document loaded, so load it there:

<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 name it:

new QaidQuests({
  endpoint: "/api/responses",
  apiKey: "qd_pk_live_...",
  container: "#estimate-form",
  fontFamily: "'Manrope', system-ui, sans-serif",
  fontSize: 16,
  questionnaire: { /* ... */ }
});

That string goes straight into --qaid-font-family, so any valid stack works, fallbacks and generic families included.

fontSize is a base size in pixels, exposed as --qaid-font-size. Push it to 17 on a roomy page or drop it to 14 for something denser. Matching your body copy is what makes the embed disappear.

Where the progress bar sits

A multi-step form shows Step 2 of 5 and a progress bar at the top by default, which works on a tall card with room around it.

Here the form fights a hero image for the fold, so the first thing read should be “What kind of project are you planning?”, not a grey bar.

progressPosition: "bottom"

That drops the counter and bar into the footer beside the Next button, leaving the question as the first text in the card.

Use "top" when the form is the page and you want to set expectations early. Use "bottom" when it is one block among several and vertical space is scarce.

The css escape hatch

The typed options cover the common cases. Past those, css is a string injected into the shadow root. Two useful overrides here: rounder inputs with a brand focus ring, and tighter card padding so the form sits between the hero and the next 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: { /* ... */ }
});

One warning about those class names. .qaid-input, .qaid-card and the rest are not public API and can change between versions without a major bump. Verify them in devtools and check again after every upgrade.

The variables are the part that stays:

  • --qaid-positive
  • --qaid-negative
  • --qaid-marker
  • --qaid-modal-width
  • --qaid-backdrop-opacity
  • --qaid-font-family
  • --qaid-font-size

Anything expressible through those, meaning colour, type and dimensions, should go through them. Reach for a class selector only where they cannot: border radius, padding, layout, animation.

Choices with photographs

The first question offers kitchen, bathroom, addition, outdoor. Four words would work; four photographs sell. Every multiple-choice option takes an image, and imageAlignment puts them above the labels or beside them.

{
  "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" }
  ]
}

"vertical" centres each image above its label, turning options into posters. It suits four to six choices where the picture carries the meaning. "horizontal" puts the image left of the label and reads as a list, better for six to twelve where the label does the work. With real project photography, take "vertical".

They render as 1:1 thumbnails, so crop square at 600x600 or better for retina, and serve WebP.

Budget, urgency, and a start date

Three numbers to collect, and a question type for each.

Budget, with currency

A USD input with guardrails and a placeholder that sets expectations:

{
  "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
}

Thousands separators appear as they type, in their locale. min and max are enforced, so somebody typing 1000 is told the project is under your minimum before you ever see it.

Urgency, with range

One to ten, a unit suffix, starting in the middle:

{
  "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"
}

unit is the suffix beside the live value, so it reads 5/10, then 7/10 as they drag. A slider beats a text box wherever the honest answer is a feeling.

Start date, with date

Set min to today’s ISO date:

{
  "id": "start_date",
  "type": "date",
  "label": "When would you like to start?",
  "min": "2026-04-27"
}

Without it, somebody can request construction starting last March. Compute min server-side, or refresh it each deploy on a static page.

autoAdvance

Every question shows a Next button by default, so choosing takes two clicks. On a single-select the second one earns nothing.

autoAdvance: true

Turn it on and the form moves the moment somebody picks a single-choice option or lets go of a slider. Clicking the kitchen photo then goes straight to the budget question.

Four types ignore it, having no moment where the answer is obviously finished: a multi-select waits for a second choice, text and currency wait for more typing, and date pickers differ enough between browsers that advancing on partial input would be hostile.

autoFocus: false in a CMS preview

The embed focuses the first input on every step, which is right on a live page.

Inside a page builder it is wrong. Every re-render of the preview iframe pulls focus out of the field the editor was typing in.

autoFocus: false

Set it in preview only. Detect the mode however you already do and leave production on the default.

The finished config

Every option above, in one block:

<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>

Drop that into a landing page section and it reads as part of the page. A preview build changes one line, autoFocus: false, and nothing else.

Back to all articles