One-paragraph summary
Line item properties are Shopify's standard mechanism for attaching custom key-value data to individual cart line items — the way personalization, customization, gift messages, and any other per-item input gets carried from the product page through the cart, checkout, order confirmation email, and admin order detail. They're part of core Shopify (no apps required to capture them), work on every plan, and are read natively by every major POD vendor (Printful, Printify, Gelato, Gooten). Properties prefixed with underscore are hidden from customer-facing surfaces but still saved on the order. This reference covers Liquid syntax for every context plus the four most common bugs.
What line item properties actually are
Every line item in a Shopify cart can carry an arbitrary number of custom key-value pairs. These pairs are called "line item properties." They originate from the customer's form submission on the product page (or via a personalizer app's API call), travel with the line item through cart and checkout, and land on the resulting Shopify order as a structured field.
The underlying data shape is a key-value map per line item. Stored in the order JSON as:
"properties": [
{ "name": "Custom text", "value": "Happy Birthday Mom" },
{ "name": "Font", "value": "Dancing Script" },
{ "name": "Color", "value": "Gold" },
{ "name": "_print_file_url", "value": "https://cdn.shopify.com/.../design-12345.png" }
]
The Liquid representation iterates as an array of key-value pairs accessible via .first and .last:
{% for property in line_item.properties %}
{{ property.first }}: {{ property.last }}
{% endfor %}
How to capture line item properties (no app)
Add an HTML form input to your product page's add-to-cart form with the name attribute prefixed by properties[]:
<label for="custom-text">Custom text:</label> <input type="text" id="custom-text" name="properties[Custom text]" maxlength="20"> <label for="font">Font:</label> <select name="properties[Font]"> <option value="Roboto">Roboto</option> <option value="Playfair Display">Playfair Display</option> <option value="Dancing Script">Dancing Script</option> </select> <label for="photo">Upload photo:</label> <input type="file" name="properties[Photo]">
When the customer submits the add-to-cart form, Shopify captures each properties[...] input as a line item property on the resulting cart line. File inputs are auto-uploaded to Shopify's Files CDN and stored as URL values.
This works without any app. For real personalization at production scale you'll want validation (character limits, file size/format checks, DPI warnings), a live preview UI, font picker, mobile-friendly upload flow, and Cart Transform-based pricing — which is what apps like Print It My Way provide on top of the same underlying line-item-properties mechanism.
Liquid reference: product page
To display line item properties already in the cart on the product page (e.g., to show "already in cart with X customization"):
{% for item in cart.items %}
{% if item.product_id == product.id %}
{% for property in item.properties %}
{% unless property.last == blank or property.first.first == '_' %}
<p>{{ property.first }}: {{ property.last }}</p>
{% endunless %}
{% endfor %}
{% endif %}
{% endfor %}
Critical: the unless clause filters two things: empty properties (Shopify creates blank properties for empty form fields by default) and underscore-prefixed properties (hidden by convention).
Liquid reference: cart page
In sections/main-cart-items.liquid or your theme's cart template:
{% for item in cart.items %}
<div class="cart-item">
<h3>{{ item.product.title }}</h3>
{% if item.properties.size != 0 %}
<ul class="cart-item-properties">
{% for property in item.properties %}
{% unless property.last == blank or property.first.first == '_' %}
<li>
<strong>{{ property.first }}:</strong>
{% if property.last contains '/uploads/' %}
<a href="{{ property.last }}" target="_blank">View file</a>
{% else %}
{{ property.last }}
{% endif %}
</li>
{% endunless %}
{% endfor %}
</ul>
{% endif %}
</div>
{% endfor %}
The property.last contains '/uploads/' check detects file URLs and renders them as clickable links instead of raw URLs.
Liquid reference: checkout (legacy checkout.liquid)
If you're still on checkout.liquid (Shopify Plus, deprecated as of Aug 2024), the syntax is the same as the cart loop above. For new Shopify Checkout Extensibility, line item properties are accessed via the Checkout UI Extensions API, not Liquid. See our Cart Transform deep dive for the modern pattern.
Liquid reference: order confirmation email
In Settings → Notifications → Order confirmation → Edit code, find the {% for line in subtotal_line_items %} loop and add:
{% for line in subtotal_line_items %}
<p>{{ line.title }} × {{ line.quantity }}</p>
{% if line.properties.size != 0 %}
{% for property in line.properties %}
{% unless property.last == blank or property.first.first == '_' %}
<p style="margin-left:20px;font-size:13px;color:#666;">
<strong>{{ property.first }}:</strong> {{ property.last }}
</p>
{% endunless %}
{% endfor %}
{% endif %}
{% endfor %}
The same loop works for the Shipping confirmation, Refund, and Abandoned cart email templates. Always test with a draft order before going live — email client rendering (Gmail, Outlook, Apple Mail) varies and inline styles are required for predictable formatting.
Liquid reference: admin order detail
The Shopify admin order detail page renders line item properties automatically — no Liquid edits needed. Underscore-prefixed properties show in a separate "Other details" expandable section. Properties with file URLs render as clickable "View file" links.
For custom admin views (Shopify Plus Flow apps, custom dashboards), access via the GraphQL Admin API:
query {
order(id: "gid://shopify/Order/123456") {
lineItems(first: 10) {
nodes {
title
customAttributes {
key
value
}
}
}
}
}
Note: in GraphQL, line item properties are called customAttributes (Shopify's API naming inconsistency). The data is identical.
Hidden properties (underscore convention)
Any property whose name starts with _ (underscore) is treated as hidden by Shopify's customer-facing surfaces (cart page, checkout, order confirmation email). The property is still saved on the order and accessible via the Admin API, Liquid templates (after explicit access), and the admin order detail page (in the "Other details" section).
Use hidden properties for production data the customer doesn't need to see:
_print_file_url— URL of the print-ready PNG/PDF for your POD vendor_design_json— raw design state (text positions, font sizes, color codes) for re-editing_personalizer_app_version— which version of the app captured this design_fulfillment_routing— internal vendor or warehouse routing tag_thumbnail_url— pre-rendered design thumbnail for fulfillment
These are not encrypted or secret. They're just visually suppressed in default surfaces. If the customer inspects the order JSON via Admin API or asks for their data under GDPR, hidden properties show. Don't put truly sensitive data in line item properties.
File uploads via line item properties
Use a standard HTML <input type="file"> in your product form with a properties[...] name:
<input type="file" name="properties[Upload your design]"
accept=".png,.jpg,.jpeg,.svg"
max-size="10485760">
Shopify's file upload flow:
- Customer selects file, browser uploads it to Shopify's Files CDN
- Shopify returns a CDN URL (https://cdn.shopify.com/...uploads/...)
- The URL is captured as the line item property value (not the binary file)
- Cart, checkout, order email show a "View file" link to the CDN URL
- POD vendor reads the URL from the order and downloads the file for production
File size limit: 20MB per upload (Shopify default). Allowed types are governed by the accept attribute and Shopify's MIME-type filter. For HEIC support (iPhone default format), you'll need an app that converts server-side — native Shopify doesn't accept HEIC.
Line item properties vs cart attributes
Both are custom key-value stores on the cart/order, but at different granularity:
| Aspect | Line item properties | Cart attributes |
|---|---|---|
| Scope | Per line item | Per cart (entire order) |
| Use case | Per-product personalization (engraving text per item) | Order-level data (delivery date for whole shipment, gift note) |
| Liquid loop | {% for property in line.properties %} | {% for attribute in cart.attributes %} |
| Form input | name="properties[Key]" | name="attributes[Key]" |
| Stays with item on split orders | Yes | Stays with parent order |
| Visible in admin | Yes, inline on line item | Yes, in order notes section |
4 common bugs and fixes
Bug 1: Empty property values show as blank lines
If you don't filter property.last == blank, every empty form field creates a blank "Key: " line in the cart and email. Always wrap your loop in:
{% unless property.last == blank %}...{% endunless %}
Bug 2: Hidden properties showing up on customer surfaces
The underscore convention is honored by Shopify's default templates but NOT automatically by your custom Liquid. If you wrote your own cart loop and forgot to filter underscores, hidden production data leaks to the customer:
{% unless property.first.first == '_' %}...{% endunless %}
The property.first.first grabs the first character of the property name to check for the underscore prefix.
Bug 3: File URLs render as raw text instead of links
Without explicit detection, file URLs show as plain text in your cart loop. Use a contains '/uploads/' check to render them as anchors:
{% if property.last contains '/uploads/' %}
<a href="{{ property.last }}" target="_blank">View file</a>
{% else %}
{{ property.last }}
{% endif %}
Bug 4: Properties lost between cart and checkout
If you have a custom checkout template (checkout.liquid on Plus, or a custom-coded section) that doesn't preserve properties, they vanish at checkout. Cart Transform–based personalization apps (Print It My Way) handle this automatically by storing personalization fees as line items. If you're seeing this with a vanilla theme, check your theme's add-to-cart JavaScript isn't stripping the properties array before submit.
When you need an app (and when you don't)
Native Shopify line item properties handle the data capture and transport perfectly. Where you need an app:
- Live preview canvas — rendering the customer's design on the product image in real time as they type
- Font picker UI — loading and previewing 35+ Google Fonts in a dropdown
- Photo upload validation — file size/format checks, HEIC support, DPI warnings before checkout
- Conditional logic — "show field B only when field A = X" without writing JavaScript
- Cart Transform pricing — adding personalization fees as clean cart line items via Shopify Functions
- Mobile UX — touch-friendly canvas, mobile camera roll integration, responsive layouts
- Admin dashboard — viewing all custom orders with inline previews
For native (no-app) line item property capture, the HTML form inputs in this guide are sufficient. For anything visual, add a personalizer app — they all use line item properties as the underlying transport, so your fulfillment workflow is identical regardless of which app you pick.
Skip the Liquid editing — install Print It My Way
Print It My Way captures all the patterns in this guide through a no-code admin: text fields, file uploads, conditional logic, Cart Transform pricing, mobile-friendly UX, and the orders dashboard with inline preview. Five minutes from install to live.
Install Print It My Way — Free Read the Cart Transform deep dive →Frequently asked questions
What are line item properties in Shopify?
Line item properties are custom key-value pairs attached to individual line items in a Shopify cart and order. They're the standard mechanism Shopify uses to carry per-item personalization data — custom text, font choices, uploaded file URLs, color selections, gift messages — from the product page through the cart, checkout, order confirmation email, and admin order detail. Personalizer apps (Print It My Way, Customily, Zakeke) use line item properties as their data-transport layer; POD vendors (Printful, Printify, Gelato) read them off orders to fulfill customized products. Line item properties are part of Shopify's core Online Store 2.0 platform and work on every Shopify plan.
What's the difference between line item properties and cart attributes?
Line item properties attach to specific line items (per-product), while cart attributes attach to the entire cart (per-order). Use line item properties when the data describes a single item — a custom mug's engraving text, a t-shirt's name and number. Use cart attributes when the data describes the whole order — a delivery date for all items, a gift note for the entire shipment, a referring sales rep's ID. Both are accessible via Shopify's Liquid templates and the Storefront/Admin APIs. Line item properties move with the item if it gets split into a sub-order; cart attributes stay attached to the parent order.
What are hidden line item properties (underscore convention)?
Line item properties prefixed with an underscore (e.g., _print_file_url, _internal_sku) are treated as hidden by Shopify — they don't display on the cart page, checkout, or order confirmation email by default, but they're still saved on the order and accessible via the Admin API and Liquid templates. Use this for production data your customer doesn't need to see: internal print file URLs, fulfillment routing tags, vendor SKUs, raw design JSON. They're not encrypted or secret — just visually suppressed in the standard customer-facing surfaces.
Are line item properties the same as product metafields?
No. Metafields are merchant-defined data attached to products, variants, customers, or orders at the catalog/account level — used for structured product specs, internal SKUs, or merchant data the customer doesn't input. Line item properties are customer-input data attached to a specific line item in a specific order — used for what the customer typed, uploaded, or selected during personalization. Metafields are configured once in the admin and apply to many orders; line item properties are unique to each order and entered by the customer at purchase time.
How do I show line item properties in order confirmation emails?
In your Shopify admin, go to Settings → Notifications → Order confirmation → Edit code. Inside the {% for line in subtotal_line_items %} loop, add the property loop with an unless clause to hide empty values and underscore-prefixed hidden properties. The same pattern works for shipping confirmation, refund, and abandoned cart emails. Test with a draft order first to verify formatting renders correctly across email clients.
How do I upload files via line item properties?
Add an HTML file input to your product form: <input type="file" name="properties[Photo]">. Shopify automatically uploads the file to its Files CDN and stores the resulting URL as the property value. The customer-facing surfaces show a clickable link to the file. Most personalizer apps (Print It My Way, Customily, Zakeke) wrap this with a friendlier UI, mobile-optimized upload, HEIC conversion, and DPI validation — but the underlying mechanism is still line item properties pointing to Shopify-hosted file URLs. File size limit is typically 20MB per upload.
Why aren't my line item properties showing up?
Six common causes: (1) property name starts with underscore and you're checking customer-facing surface where they're hidden; (2) the Liquid loop is inside the wrong template; (3) the empty-blank check is filtering out legitimate empty values; (4) theme uses cached snippets that didn't pick up your edit; (5) personalizer app is using metafields or a custom storage layer instead of line item properties; (6) the property name has special characters or non-ASCII chars that need URL encoding. Verify by checking the order JSON via Admin API — if it's in the order, it's somewhere; if not, the form submission didn't capture it.