JUST Docs
WidgetInstallationHeadless storefronts

Next.js and custom storefronts

Add the JUST widget to a headless Shopify storefront built with Next.js or another custom frontend.

On a Next.js or custom storefront the Shopify globals are absent, so the host wires the shop, locale, cart and consent explicitly. The integration stays client-side: one script and one <just-ai-agent> web component, mounted once and kept across route changes.

The steps below take the wires in order (the element, its attributes, your cart, the analytics hooks, your own entry points) and end on a complete example that carries all of them. Every attribute this page sets is documented in the attributes reference.

Prerequisites

  • The JUST Shopify app installed and connected on your Shopify store.
  • Your Shopify shop domain, for example your-shop.myshopify.com.

Install

Load the widget script once and render the <just-ai-agent> element, for example in your root layout:

// app/layout.tsx
<Script
  type="module"
  src="https://agent.getjust.eu/ai-commerce-agent-widget.js"
  strategy="afterInteractive"
/>

<just-ai-agent shop="your-shop.myshopify.com" headless="true" />

Replace your-shop.myshopify.com with your Shopify shop domain. The JUST trigger bar now appears on every page.

Product-page detection is URL-based

The widget recognizes product pages by their /products/{handle} path only. Storefronts with localized routes (/produits/…) lose product-page features: the immersive view, suggested questions, and the product block.

Allow the widget through your CSP

If the storefront sends a Content Security Policy, allow https://agent.getjust.eu in script-src, and allow both https://agent.getjust.eu and https://agent-stream.getjust.eu in connect-src. JUST's optional configured fonts also need https://fonts.gstatic.com in font-src, and its Shadow DOM and Global CSS require inline styles.

Do not replace the framework's existing sources when extending a directive. Keep its own scripts, connections and fonts, then append the JUST origins.

Strict CSP and Global JS

A shop-level Global JS override is injected as an inline script without your application's nonce. A nonce-only CSP blocks that optional override; the widget itself continues to work without Global JS.

Configure

Configure the widget with attributes on <just-ai-agent>. Two are required: shop and headless="true". Set locale and country explicitly too, because a headless storefront gives the widget no way to detect them:

<just-ai-agent
  shop="your-shop.myshopify.com"
  headless="true"
  locale="en"
  country="US"
/>

country (ISO 3166-1 alpha-2) selects prices and currency; without it, prices come from your shop's default market. The full attribute table is in the attributes reference, and appearance (theme, brand color, font, radius) is covered in Customization.

Set locale explicitly

When the locale attribute is absent, the widget falls back to the page's lang attribute and then to fr. A non-French storefront that omits locale can ship a French widget.

CSS overrides

The widget renders inside a Shadow DOM, so your storefront CSS cannot reach its internals. Two CSS custom properties are supported on the host element:

just-ai-agent {
  --ai-z-index: 100; /* render below your own drawers and modals */
  --ai-bottom-position: 80px; /* clear a sticky bottom bar */
}

Anything else inside the widget is internal and may change without notice. See the styling reference.

At boot the widget also appends one shop-scoped stylesheet to document.head: <style data-just-ai="custom">, the Global CSS override configured in the dashboard, removed when the shop has none. A shop can carry a script twin too: <script data-just-ai="custom-js">, the Global JS override, executed once per page load. If your frontend manages <head> contents, leave both tags alone.

Connect your cart

On a headless storefront, your cart remains the source of truth. The widget emits two DOM events on the <just-ai-agent> element; a small bridge component answers them with your own cart implementation:

  • cart-add-requested: the widget asks your storefront to add a variant to the cart. The detail carries { variantId, quantity }: variantId is a Shopify ProductVariant GID, usable as the Storefront API merchandiseId, and a missing quantity counts as 1.
  • cart-clicked: the widget asks your storefront to open the cart experience. Close the widget in this handler, so the panel does not cover your cart drawer.

In the other direction, keep the cart-count attribute in sync with your cart quantity: a non-negative integer, where a missing or invalid value hides the badge.

The complete example below ships this bridge as one drop-in component, JustBridge, with the analytics hooks of the next step already wired.

Pages with a product block

If a page also renders a product block, a second <just-ai-agent mode="block"> element, every selector in your bridge must target the standalone instance: document.querySelector('just-ai-agent:not([mode="block"])'). Otherwise cart events land on the wrong element and add-to-cart dies silently.

Set up analytics

The widget sends its analytics directly from the storefront. Some dashboard metrics work out of the box; add-to-cart rates need the cart-added event, and revenue attribution needs the cart-id attribute. Both are wired below. Analytics explains what the dashboard measures and why each wire matters.

The tracking-consent attribute controls analytics collection. When it is "false", the widget sends no analytics and stores no visitor identifier. The assistant itself is not affected and remains fully functional.

If your storefront requires opt-in consent (for example under GDPR), connect the attribute to your consent platform and render "false" until the shopper has opted in:

<just-ai-agent
  shop="your-shop.myshopify.com"
  headless="true"
  tracking-consent={trackingConsent ? 'true' : 'false'}
/>

When the attribute is omitted, analytics are enabled. The attribute can be updated at runtime; no page reload is required.

Report add-to-cart events

Report each successful add-to-cart by dispatching a cart-added event on the <just-ai-agent> element:

export function reportCartAdded(variantId?: string, quantity?: number) {
  widget()?.dispatchEvent(
    new CustomEvent('cart-added', { detail: { variantId, quantity } }),
  );
}

Call it for every add-to-cart on the storefront (product page, quick-add, cart drawer), not only the ones requested by the agent. Both payload fields are optional; a missing quantity counts as 1.

Enable purchase attribution

Set the cart-id attribute to your cart identifier and update it when the cart is created or replaced:

useEffect(() => {
  if (cartId) {
    widget()?.setAttribute('cart-id', cartId);
  }
}, [cartId]);

Pass cart-id verbatim

cart-id must be passed exactly as returned by the Storefront API, including the ?key= parameter. A "cleaned" id silently breaks purchase attribution and the cart confirmation card.

The widget stores its visitor identifier on the cart as a hidden cart attribute (_ai_agent_visitor_id); existing cart attributes are preserved. Purchases are then attributed to the visitor who made them automatically, and to their conversation when they had one.

Open and close the widget

Open the widget from your own UI (a button, a search form, a campaign entry point) by dispatching just:ai-agent:open on window, optionally with a first message:

window.dispatchEvent(
  new CustomEvent('just:ai-agent:open', {
    detail: { message: 'Help me choose a gift', source: 'header_cta' },
  }),
);

Close it by dispatching just:ai-agent:close on window; the panel closes back to the trigger bar. Both events are safe to dispatch before the widget finishes loading; the most recent instruction wins.

window.dispatchEvent(new Event('just:ai-agent:close'));

See entry points for the built-in ways to open the panel, and the events reference for the full event contract.

Recognize the logged-in customer

When a shopper is logged into a customer account, pass their email with the customer-email attribute, set before the widget mounts, because it is read once, at session creation:

<just-ai-agent
  shop="your-shop.myshopify.com"
  headless="true"
  customer-email={customer?.email}
/>

Omit it for a guest. With it set, guided flows skip asking for an email the shopper already gave you. customer-id currently has no effect on a headless storefront: the agent only recognizes a customer Shopify confirms, which needs a Shopify theme.

Complete example

The two files below carry the full integration: element, cart bridge, and all analytics hooks.

// app/layout.tsx
import Script from 'next/script';
import { ReactNode } from 'react';
import { JustBridge } from '@/components/just-bridge';

export default function RootLayout({ children }: { children: ReactNode }) {
  const trackingConsent = true; /* read the stored choice from your consent platform */

  return (
    <html lang="en">
      <body>
        {children}

        <Script
          type="module"
          src="https://agent.getjust.eu/ai-commerce-agent-widget.js"
          strategy="afterInteractive"
        />

        <just-ai-agent
          shop="your-shop.myshopify.com"
          headless="true"
          locale="en"
          country="US"
          tracking-consent={trackingConsent ? 'true' : 'false'}
        />

        <JustBridge />
      </body>
    </html>
  );
}
// components/just-bridge.tsx
'use client';

import { useEffect } from 'react';
import { useCart } from '@/commerce/cart';

type CartAddRequestedDetail = { variantId: string; quantity: number };

const widget = () =>
  document.querySelector('just-ai-agent:not([mode="block"])');

// Call after every successful add-to-cart on the storefront.
export function reportCartAdded(variantId?: string, quantity?: number) {
  widget()?.dispatchEvent(
    new CustomEvent('cart-added', { detail: { variantId, quantity } }),
  );
}

export function JustBridge() {
  const { cart, addLineItemToCart, openCart } = useCart();
  const cartCount = cart?.totalQuantity ?? 0;
  const cartId = cart?.id;

  useEffect(() => {
    const element = widget();
    if (!element) return;

    const onAddRequested = async (event: Event) => {
      const { variantId, quantity } = (
        event as CustomEvent<CartAddRequestedDetail>
      ).detail;

      const updatedCart = await addLineItemToCart(variantId, quantity || 1);

      // Set cart-id before reporting the add. On a first add the cart is
      // created by the call above, so the effect below would set the
      // attribute one render too late.
      if (updatedCart?.id) {
        widget()?.setAttribute('cart-id', updatedCart.id);
      }

      reportCartAdded(variantId, quantity || 1);
    };

    const onCartClicked = () => {
      openCart();
      window.dispatchEvent(new Event('just:ai-agent:close'));
    };

    element.addEventListener('cart-add-requested', onAddRequested);
    element.addEventListener('cart-clicked', onCartClicked);

    return () => {
      element.removeEventListener('cart-add-requested', onAddRequested);
      element.removeEventListener('cart-clicked', onCartClicked);
    };
  }, [addLineItemToCart, openCart]);

  useEffect(() => {
    widget()?.setAttribute('cart-count', String(cartCount));
  }, [cartCount]);

  useEffect(() => {
    if (cartId) {
      widget()?.setAttribute('cart-id', cartId);
    }
  }, [cartId]);

  return null;
}

Replace useCart, addLineItemToCart, and openCart with your storefront's cart implementation, and call reportCartAdded from your own add-to-cart actions. Have addLineItemToCart return the updated cart. The cartId effect stays as it is, for carts created or replaced outside an add.

Testing

  1. The JUST trigger bar appears; open it, send a message, and confirm product-aware answers.
  2. Add a product from the chat: your bridge receives cart-add-requested, the cart updates, the badge follows cart-count, and the widget shows a confirmation card with the product and the cart total. Repeat on an empty cart to cover the first add.
  3. The cart entry point in the widget opens your cart UI.
  4. A product-page add dispatches cart-added on <just-ai-agent>, and shows no card in the chat.
  5. Once a cart exists, the cart-id attribute is set on <just-ai-agent>.
  6. With consent declined, <just-ai-agent> renders tracking-consent="false" and no requests to /api/analytics/track are sent; chat still works.
  7. Start checkout and confirm the agent-added item is present.

The widget hides on session failure

When session creation fails (wrong shop domain, or a shop not enabled on JUST) the widget hides entirely instead of showing an error. If the trigger bar never appears, check the shop attribute first.

Analytics appear on your dashboard within about two hours of shopper activity.

Troubleshooting

  • Unexpected token 'export': confirm the script still has type="module".
  • A CSP error names agent.getjust.eu: add the origin to the directive named by the browser without removing the framework's existing sources.
  • The configured font falls back: allow https://fonts.gstatic.com in font-src.
  • The script loads but the widget disappears: check the console for Shop not found, then verify the exact shop domain and its activation in the same JUST environment.
  • TypeScript rejects <just-ai-agent>: add it to React's JSX.IntrinsicElements in the project's declarations.

On this page