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
- 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.
- Replace the generated shopify.extension.toml and the two files in src/ with the files below.
- Run the fixtures with shopify app function run --input tests/tagged-product.input.json.
- Deploy with shopify app deploy and create the customization with deliveryCustomizationCreate.
- Set the $app:function-configuration metafield, for example {"productTags": ["oversized"], "optionTitles": ["Express", "Overnight"]}.
Code
shopify.extension.tomlapi_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.graphqlquery 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.
Shopify documentation
- 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.
