Hide shipping rates by product tag

A Shopify delivery customization Function that hides chosen shipping options when the cart has a product with a given tag, such as oversized or hazmat.

Function API
Delivery Customization
Target
cart.delivery-options.transform.run
Plan
Any plan in a public app; Shopify Plus in a custom app
App scope
write_delivery_customizations
API version
2026-10
Last checked

In short

This delivery customization Function hides the shipping options you name when a shipment contains a product with one of your tags, such as oversized or hazmat. Tags and option names live in one JSON metafield that also feeds an input query variable. It uses the cart.delivery-options.transform.run target of the Delivery Customization API, version 2026-10.

How to use it

  1. Create an app with Shopify CLI 4.9.0 or later (needed for target-level input_variables), then add the extension: shopify app generate extension --template delivery_customization, with JavaScript.
  2. Replace the generated shopify.extension.toml and the two files in src/ with the files below.
  3. Run the fixtures with shopify app function run --input tests/tagged-product.input.json.
  4. Deploy with shopify app deploy and create the customization with deliveryCustomizationCreate.
  5. Set the $app:function-configuration metafield, for example {"productTags": ["oversized"], "optionTitles": ["Express", "Overnight"]}.

Code

shopify.extension.toml
api_version = "2026-10"

[[extensions]]
name = "Hide shipping rates by product tag"
handle = "hide-rates-by-product-tag"
type = "function"

  [[extensions.targeting]]
  target = "cart.delivery-options.transform.run"
  input_query = "src/cart_delivery_options_transform_run.graphql"
  input_variables = { namespace = "$app", key = "function-configuration" }
  export = "cart-delivery-options-transform-run"

  [extensions.build]
  command = ""
  path = "dist/function.wasm"
src/cart_delivery_options_transform_run.graphql
query CartDeliveryOptionsTransformRunInput($productTags: [String!]) {
  cart {
    deliveryGroups {
      cartLines {
        merchandise {
          __typename
          ... on ProductVariant {
            product {
              hasAnyTag(tags: $productTags)
            }
          }
        }
      }
      deliveryOptions {
        handle
        title
      }
    }
  }
  deliveryCustomization {
    metafield(namespace: "$app", key: "function-configuration") {
      jsonValue
    }
  }
}
src/cart_delivery_options_transform_run.js
// @ts-check

/**
 * @typedef {import("../generated/api").CartDeliveryOptionsTransformRunInput} CartDeliveryOptionsTransformRunInput
 * @typedef {import("../generated/api").CartDeliveryOptionsTransformRunResult} CartDeliveryOptionsTransformRunResult
 */

/**
 * Settings stored as JSON in the customization's metafield. The same
 * metafield feeds the query's $productTags variable (see input_variables in
 * shopify.extension.toml), so tags are checked by Shopify, not in this code.
 */
const DEFAULT_CONFIG = {
  // Matched case-insensitively anywhere in the delivery option title.
  optionTitles: /** @type {string[]} */ ([]),
};

/**
 * @param {CartDeliveryOptionsTransformRunInput} input
 * @returns {CartDeliveryOptionsTransformRunResult}
 */
export function cartDeliveryOptionsTransformRun(input) {
  const config = { ...DEFAULT_CONFIG, ...input.deliveryCustomization?.metafield?.jsonValue };
  const titles = config.optionTitles.map((title) => title.toLowerCase());

  // Only the shipment that contains a tagged product loses the options; other
  // delivery groups in a split cart keep theirs.
  const operations = input.cart.deliveryGroups
    .filter((group) =>
      group.cartLines.some(
        (line) => line.merchandise.__typename === "ProductVariant" && line.merchandise.product.hasAnyTag,
      ),
    )
    .flatMap((group) => group.deliveryOptions)
    .filter((option) => titles.some((title) => (option.title ?? "").toLowerCase().includes(title)))
    .map((option) => ({ deliveryOptionHide: { deliveryOptionHandle: option.handle } }));

  return { operations };
}
Test input and expected output
tests/no-tagged-product.input.json
{
  "cart": {
    "deliveryGroups": [
      {
        "cartLines": [
          { "merchandise": { "__typename": "ProductVariant", "product": { "hasAnyTag": false } } }
        ],
        "deliveryOptions": [
          { "handle": "9f8e-ground", "title": "Ground" },
          { "handle": "9f8e-express", "title": "Express" },
          { "handle": "9f8e-overnight", "title": "Overnight" }
        ]
      }
    ]
  },
  "deliveryCustomization": {
    "metafield": { "jsonValue": { "productTags": ["oversized", "hazmat"], "optionTitles": ["Express", "Overnight"] } }
  }
}
tests/no-tagged-product.output.json
{ "operations": [] }
tests/tagged-product.input.json
{
  "cart": {
    "deliveryGroups": [
      {
        "cartLines": [
          { "merchandise": { "__typename": "ProductVariant", "product": { "hasAnyTag": false } } },
          { "merchandise": { "__typename": "ProductVariant", "product": { "hasAnyTag": true } } }
        ],
        "deliveryOptions": [
          { "handle": "9f8e-ground", "title": "Ground" },
          { "handle": "9f8e-express", "title": "Express" },
          { "handle": "9f8e-overnight", "title": "Overnight" }
        ]
      }
    ]
  },
  "deliveryCustomization": {
    "metafield": { "jsonValue": { "productTags": ["oversized", "hazmat"], "optionTitles": ["Express", "Overnight"] } }
  }
}
tests/tagged-product.output.json
{
  "operations": [
    { "deliveryOptionHide": { "deliveryOptionHandle": "9f8e-express" } },
    { "deliveryOptionHide": { "deliveryOptionHandle": "9f8e-overnight" } }
  ]
}

Written by Lintel. MIT license: free to use in your own store and in client work.

Limits and edge cases

  • productTags is read twice: Shopify uses it as the $productTags query variable, and the code reads optionTitles from the same JSON.
  • If the metafield isn’t set, the variable is null and no option is hidden.
  • A list variable can hold at most 100 tags, and the metafield must stay under 10,000 bytes or Shopify won’t return it.
  • Option titles are matched anywhere in the title, ignoring case, so "Express" also matches "UPS Express Saver".

Questions

How do I hide a shipping method for certain products in Shopify?
Tag the products, then use a delivery customization Function that checks the tag with hasAnyTag and hides the option, as this example does.
Why use input query variables instead of writing the tags in the query?
Tags written in the query need a new deploy to change and are the same for every store. With a variable, each customization reads its tags from its own metafield.
Can I hide pickup instead of a carrier rate?
Yes. Put the pickup option’s title in optionTitles. To be stricter, add deliveryMethodType to the query and match PICK_UP.
  • Written for Function API version 2026-10 and checked against Shopify’s reference on the date shown. Shopify releases a new version every quarter.
  • Custom apps that contain Functions only run on Shopify Plus stores; a public app with the same Function runs on any plan unless the page says otherwise.
  • Every example ships with test fixtures, and we run them before publishing. Run them again after you change the code: shopify app function run.
  • MIT license. Use the code in your own store or in client work.

Need this built, tested and maintained for your store? See Shopify Functions development.

Have something to build? Let’s talk it through.

Taking a limited number of projects at a time. Every brief is read by an engineer, not a salesperson.

Gencer KarakayaShopify Engineer

An engineer reads your brief, and we reply by email.