> ## Documentation Index
> Fetch the complete documentation index at: https://docs.heylua.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Style the widget

> Match the widget to your brand with the theme, button, and header options, then reach anything else with CSS inside its shadow root

After this guide, the widget uses your colors, button, and header in both light and dark mode. Use the options first and CSS only for what they don't cover: the widget renders inside a shadow root that your stylesheets never touch. Each option is described in the [configuration reference](/channels/web-widget/configuration).

**Before you begin**

* The widget installed and working, per the [quickstart](/channels/web-widget/quickstart).
* Your brand color as a CSS color value and, optionally, a 16 × 16 px icon at a public HTTPS URL.

<Steps>
  <Step title="Pin or follow the theme">
    `theme` defaults to `"auto"`, which follows the end user's operating-system preference; pin it when your site has one look.

    ```js theme={null}
    window.LuaPop.init({
      agentId: "agent_abc123",
      environment: "production",
      theme: "dark",
    });
    ```
  </Step>

  <Step title="Brand the button">
    `buttonColor` sets the background, `buttonText` the label, and `buttonIcon` an image URL that replaces the Lua logo. Emoji go in the text, never in the icon.

    ```js theme={null}
    window.LuaPop.init({
      agentId: "agent_abc123",
      environment: "production",
      buttonColor: "#6d28d9",
      buttonText: "Ask Acme",
      buttonIcon: "https://www.example.com/chat-icon.png",
      popupButtonStyles: {
        borderRadius: "12px",
        height: "44px",
        padding: "0 16px",
        fontSize: "14px",
      },
    });
    ```

    `popupButtonStyles` are inline styles, so use camelCase keys and plain values; a `background` here wins over `buttonColor`. Without `buttonColor`, the button uses the theme's primary color, which no option sets; change that only with CSS in the shadow root (last step).
  </Step>

  <Step title="Place the button">
    `position` picks a corner, `popupButtonPositionalContainerStyles` adjusts the offsets, and `draggable: true` lets end users move the button when it covers something on the page.

    ```js theme={null}
    window.LuaPop.init({
      agentId: "agent_abc123",
      environment: "production",
      position: "bottom-left",
      popupButtonPositionalContainerStyles: { bottom: "24px", left: "24px" },
      draggable: true,
    });
    ```
  </Step>

  <Step title="Style the header">
    `chatTitle` and `chatTitleHeaderStyles` cover the header bar; `chatHeaderSubtitle` controls the "Powered by" line under it.

    ```js theme={null}
    window.LuaPop.init({
      agentId: "agent_abc123",
      environment: "production",
      chatTitle: "Acme support",
      chatTitleHeaderStyles: { background: "#6d28d9", color: "#ffffff" },
      chatHeaderSubtitle: { visible: false },
    });
    ```
  </Step>

  <Step title="Optional: inject CSS into the shadow root">
    The widget mounts an open shadow root on `#lua-shadow-root` (`#lua-shadow-root-embedded` in embedded mode). After `init()` resolves, append a `<style>` element to it.

    ```js theme={null}
    const widget = await window.LuaPop.init({
      agentId: "agent_abc123",
      environment: "production",
    });

    const shadowRoot = document.getElementById("lua-shadow-root")?.shadowRoot;
    if (widget && shadowRoot) {
      const style = document.createElement("style");
      style.textContent = `
        .lua-pop-button { box-shadow: 0 8px 24px rgba(109, 40, 217, 0.35); }
        .lua-pop-chat { border-radius: 20px; }
        .lua-pop-chat-header { border-bottom: 2px solid #6d28d9; }
      `;
      shadowRoot.appendChild(style);
    }
    ```

    Class names in the deployed build: `.lua-pop-widget` on the fixed container, `.lua-pop-button`, `.lua-pop-chat`, `.lua-pop-chat-header`, `.lua-close-button`, and `.lua-pop-chat-mobile-fullscreen` on screens under 640 px and on phones and tablets. The composer is `.lua-pop-input-region` and its textarea has the id `lua-pop-chat-input`. Message bubbles carry `.chat-message-bubble` with `.chat-message-bubble-user` or `.chat-message-bubble-agent`, and rendered Markdown sits in `.chat-markdown-content`.

    <Warning>
      Class names are not a stable API, and the hosted script has no version pin: `lua-pop.umd.js` is replaced on every widget release. Keep shadow-root CSS small and recheck it after releases.
    </Warning>
  </Step>

  <Step title="Verify">
    Reload the page, open the widget, and switch your operating system between light and dark mode. The button, the header, and any injected rules hold in both.
  </Step>
</Steps>

## If it isn't working

<AccordionGroup>
  <Accordion title="My stylesheet has no effect on the widget">
    Page CSS cannot cross the shadow boundary. Use the options from the earlier steps, or inject a `<style>` into the shadow root as in the last step.
  </Accordion>

  <Accordion title="buttonColor is ignored">
    A `background` in `popupButtonStyles` overrides it. Set one or the other.
  </Accordion>

  <Accordion title="The button is not where position says">
    With `draggable: true`, the spot an end user chose is stored in their browser under the `localStorage` key `luapop:pos:<agentId>` and wins over `position`. Remove the key to reset it.
  </Accordion>
</AccordionGroup>

## Next steps

<Columns cols={2}>
  <Card title="Widget configuration" href="/channels/web-widget/configuration">Every display option with its default.</Card>
  <Card title="Frameworks" href="/channels/web-widget/frameworks">Mount and clean up in React, Next.js, Vue, Angular, and Svelte.</Card>
</Columns>
