---
title: Create a custom Liquid block for nested cart lines
description: >-
  Learn how to create nested cart lines for extended warranties using the Cart
  AJAX API and metafields
source_url:
  html: >-
    https://shopify.dev/docs/apps/build/product-merchandising/nested-cart-lines/tutorial
  md: >-
    https://shopify.dev/docs/apps/build/product-merchandising/nested-cart-lines/tutorial.md
---

# Create a custom Liquid block for nested cart lines

This tutorial covers how to create nested cart lines for extended warranties using the Cart AJAX API and metafields with a custom Liquid block.

***

## 1.​Create variant metafield definition for extended warranties

First, you'll need to create a metafield definition that maps a product to their available extended warranty options.

Create a metafield definition named `extended_warranty` on the product object and set the metafield type to a list of product variants.

![Metafield definition for extended warranty](https://shopify.dev/assets/assets/images/merchandising/nested-cart-lines-metafield-definition-CCI1pqrR.png)

This metafield will store references to warranty product variants that can be offered for each product variant. This can be configured in the Shopify admin product details page.

![Metafield definition for extended warranty](https://shopify.dev/assets/assets/images/merchandising/nested-cart-lines-metafield-use-paE8tQON.png)

***

## 2.​Custom Liquid block for creating nested cart line

Next, you'll create custom Liquid block that reads the extended warranty metafield and displays the available warranty options to customers.

On your theme editor, add a custom Liquid block on the product details page.

![How to create a custom Liquid block](https://shopify.dev/assets/assets/images/merchandising/nested-cart-lines-custom-liquid-block-juxHlPMW.png)

Paste the following snippet:

## Custom Liquid block

```liquid
{% assign current_variant = product.selected_or_first_available_variant %}
{% assign warranties = product.metafields.custom.extended_warranty.value %}


<div className="warranty-widget-{{ block.id }}">
  <h3>Protect Your Purchase</h3>


  <div className="warranty-options">
    {% for warranty in warranties %}
      <label>
        <input type="checkbox" value="{{ warranty.id }}" />
        <span>{{ warranty.title }}</span>
        <span className="price">{{ warranty.price | money }}</span>
      </label>
    {% else %}
      <p>No warranty options available</p>
    {% endfor %}
  </div>


  <button className="add-btn" data-variant="{{ current_variant.id }}" {% unless warranties %}disabled{% endunless %}>
    Add to Cart
  </button>
</div>


<script>
{
  const widget = document.querySelector('.warranty-widget-{{ block.id }}');
  const options = widget.querySelector('.warranty-options');
  const btn = widget.querySelector('.add-btn');


  btn.onclick = async () => {
    const selected = [...options.querySelectorAll(':checked')];
    if (!selected.length) return alert('Please select a warranty');


    const variantId = btn.dataset.variant;
    const items = [
      { id: variantId, quantity: 1 },
      ...selected.map(input => ({ id: input.value, quantity: 1, parent_id: variantId }))
    ];


    btn.disabled = true;
    btn.textContent = 'Adding...';


    try {
      await fetch('/cart/add.js', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ items })
      });
      location.href = '/cart';
    } catch {
      btn.textContent = 'Error';
      setTimeout(() => (btn.textContent = 'Add to Cart', btn.disabled = false), 2000);
    }
  };
}
</script>


<style>
  .warranty-widget-{{ block.id }} { margin: 20px 0; padding: 20px; border: 1px solid #ddd; }
  .warranty-widget-{{ block.id }} label { display: block; margin: 10px 0; cursor: pointer; }
  .warranty-widget-{{ block.id }} input { margin-right: 10px; }
  .warranty-widget-{{ block.id }} .price { float: right; color: #666; }
  .warranty-widget-{{ block.id }} .add-btn {
    width: 100%; padding: 12px; background: #000; color: #fff;
    border: none; cursor: pointer; margin-top: 15px;
  }
  .warranty-widget-{{ block.id }} .add-btn:disabled { opacity: 0.5; cursor: not-allowed; }
</style>
```

Once saved, the block will be rendered on the product details page and the "Add to Cart" button will enable multiple product variants to be added to the cart, with selected variants containing a reference to the parent cart line.

![Snippet rendering product variant options for warranties.](https://shopify.dev/assets/assets/images/merchandising/nested-cart-lines-block-DiyqmRiL.png)

When added to the cart, nested cart lines will have distinct representation on Checkout.

![Checkout with a nested cart line. The leash has a warranty as its nested item](https://shopify.dev/assets/assets/images/merchandising/nested-cart-lines-checkout-BGRZdfIy.png)

**Note:**

If you want to build validation to ensure that only eligible products were nested, the [Cart and Checkout Validation Function](https://shopify.dev/docs/api/functions/latest/cart-and-checkout-validation) can be leveraged.

***

## 3.​Defining nesting rules

You can use custom data like custom data like [product reference lists](https://help.shopify.com/en/manual/custom-data/metafields/metafield-definitions/metafield-lists) to define nesting rules. Depending on the use case, you can either:

* List all the products (or variants) a specific product (or variant) can be nested under.
* List all the products (or variants) that can be nested under a specific product (or variant).

This data can be read by:

* Themes in order to display the right products.
* Cart validation functions in order to validate the data before nesting the product under another one in the cart.

**Note:**

It is important to validate the cart using functions in order to ensure someone trying to manipulate the storefront using the cart API directly can't bypass the rules defined by the merchant through the app. Same goes for quantity and line splitting rules.

In this example, checkout and cart validation function has been deployed with an app for checking allowed nested cart lines. A metafield, `nestable_products` contains a list of allowed products that can be nested.

## Example Cart Validation function

## run.graphql

```graphql
query RunInput {
  cart {
    lines {
      id
      quantity
      lineRelationship {
        parent {
          id
          quantity
          merchandise {
            ...MerchandiseFields
          }
        }
      }
      merchandise {
        ...MerchandiseFields
      }
    }
  }
}


fragment MerchandiseFields on Merchandise {
  ... on ProductVariant {
    id
    metafield(key: "nestable_variants") {
      value
      type
      jsonValue
    }
    title
    product {
      id
      title
      metafield(key: "nestable_products", namespace: "custom") {
        value
        type
        jsonValue
      }
    }
  }
}
```

## run.ts

```typescript
import type {
    RunInput,
    CartValidationsGenerateRunResult,
    ValidationError,
    MerchandiseFields,
  } from "../generated/api";


    type ExtractMerchandise<T> = T extends { id: string }
	? T
	: never;
    type Merchandise = ExtractMerchandise<MerchandiseFields>;


    function performValidationChecks(lineMerch: Merchandise, parentMerch: Merchandise): boolean {
    const childProductId = lineMerch.product?.id;
    // This is a metafield that contains an array of product ids that are compatible with the parent product
    const compatibleChildProducts = parentMerch.product?.metafield?.jsonValue;


    // Check if the child product is in the list of compatible child products
    return compatibleChildProducts?.includes(childProductId) ?? false;
  }


  export function cartValidationsGenerateRun(input: RunInput): CartValidationsGenerateRunResult {
    const errors: ValidationError[] = [];


    for (const line of input.cart.lines) {
      const parent = line.lineRelationship?.parent;


      if (!parent) continue;


      const lineMerch = line.merchandise;
      const parentMerch = parent.merchandise;


      const isValid = performValidationChecks(lineMerch, parentMerch);
      if (!isValid) {
        errors.push({
          message: `Parent ${parentMerch.title} is not compatible with child ${lineMerch.title}`,
          target: "$.cart",
        });
      }
    }


    const operations = [
      {
        validationAdd: {
          errors,
        },
      },
    ];


    return { operations };
  };
```

***
