Bigcommerce Headless Sticky Add to Cart

Step 1: Create an opt7 directory

Create a directory under components like components/opt7.

Step 2: Create opt7 files

components
├── opt7
│   └── sticky-atc
│       └── index.tsx

index.tsx

'use client';

import { ShoppingCart, Loader2 as Spinner } from 'lucide-react';
import { useTranslations } from 'next-intl';
import React, { useEffect, useState } from 'react';
import { useFormContext } from 'react-hook-form';

import { getProduct } from '~/client/queries/get-product';
import { BcImage } from '~/components/bc-image';
import { QuantityField } from '~/components/product-form/fields/quantity-field';
import { Button } from '~/components/ui/button';

type Product = Awaited<ReturnType<typeof getProduct>>;

const StickyATC = ({ product, id }: { product: NonNullable<Product>; id: string }) => {
    const t = useTranslations('Product.Form');
    const { formState } = useFormContext();
    const { isSubmitting } = formState;

    const [isVisible, setIsVisible] = useState(false);

    useEffect(() => {
        const handleScroll = () => {
            const element = document.getElementById(id);

            if (element) {
                const bounding = element.getBoundingClientRect();

                const isInViewport =
                    bounding.top >= 0 &&
                    bounding.left >= 0 &&
                    bounding.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
                    bounding.right <= (window.innerWidth || document.documentElement.clientWidth);

                setIsVisible(!isInViewport);
            }
        };

        window.addEventListener('scroll', handleScroll);

        return () => {
            window.removeEventListener('scroll', handleScroll);
        };
    }, [id]);

    return (
        <div
            className={`fixed bottom-0 left-1/2 z-50 flex h-0  w-full -translate-x-1/2 transform bg-white shadow-[-28px_10px_80px_-19px_#000] transition-all duration-300  ${isVisible ? 'flex h-[70px]' : 'hidden'}`}
            id={id}
        >
            <div className="product-properties absolute inset-0 m-auto flex h-full w-full justify-between gap-[6px] px-[16px] py-[8px]">
                <div className="image hidden h-[54px] w-[54px] rounded border border-gray-500 lg:block">
                    {product.defaultImage ? (
                        <BcImage
                            alt={product.defaultImage.altText}
                            className="h-full object-contain"
                            height={54}
                            priority={true}
                            src={product.defaultImage.url}
                            width={54}
                        />
                    ) : null}
                </div>
                <div className="title grid w-auto place-items-center text-[#0f172a]">
                    <h2 className="m-0 line-clamp-3 overflow-hidden p-0 text-15 font-semibold">
                        {product.name}
                    </h2>
                </div>
                <div className="addToCart flex w-[140px] min-w-[140px] items-center justify-end gap-[40px] md:min-w-[280px]">
                    <QuantityField showLabel={false} />
                    <Button
                        className="mx-0 h-[40px] w-[44px] max-w-56 rounded px-3 py-0 text-15 font-bold leading-[1] text-white hover:border-transparent hover:bg-primary sm:text-15 md:min-w-[123px]"
                        disabled={product.availabilityV2.status === 'Unavailable' || isSubmitting}
                        type="submit"
                    >
                        {isSubmitting ? (
                            <>
                                <Spinner aria-hidden="true" className="animate-spin" />
                                <span className="sr-only">{t('processing')}</span>
                            </>
                        ) : (
                            <div className="flex items-center gap-1">
                                <div className="hidden md:block">{t('addToCart')}</div>
                                <ShoppingCart size={16} />
                            </div>
                        )}
                    </Button>
                </div>
            </div>
        </div>
    );
};

export default StickyATC;

Usage

Add this component to product form component

components
├── product-form
│   └── index.tsx
.
.
import StickyATC from '../opt7/sticky-atc';
.
.
export const ProductForm = ({
    product,
    showPriceRange,
}: {
    product: Product;
    showPriceRange: boolean;
}) => {
    .
    .
    .
    return (
        .
        .
        <FormProvider handleSubmit={handleSubmit} register={register} {...methods}>
            .
            .
            .
            <StickyATC id="opt7ATC" product={product} />
        </FormProvider>
    )
}

** The id we specified is the id of the add to cart button in the product form.

Happy Hacking! 🧡