Back to portfolio

Blog

How to update a Shopify theme without losing your customizations or custom components

Update a customized Shopify theme without losing custom code, sections or components: inventory, git merge, Shopify CLI, Liquid and staging tests.

  • shopify
  • liquid
  • shopify-cli
  • tailwind
  • git
  • theme-update
  • custom-code
  • custom-components
Title card for the guide on updating a customized Shopify theme with git, Shopify CLI and Liquid

Updating a customized Shopify theme requires checking what the update preserves and what needs manual migration. Shopify copies theme editor settings and can carry over code changes that do not conflict with the new version. If it cannot include those changes, adapt and transfer them to an unpublished theme. Installing a clean vendor copy also requires migrating customizations.

This guide presents a manual migration workflow with Git, Liquid and Shopify CLI examples. It follows the stages of my work for Charlotte’s Web: section inventory, migration of settings and custom code, style adaptation and full workflow testing in staging. The examples are educational; they do not reproduce client code. My delivery on that project reached staging; the publication step in this guide is a general recommendation.

What a Shopify theme update preserves

Two kinds of customization live in a theme, and they behave differently:

Where it lives Examples What an upgrade does to it
Code files (custom sections, components, snippets) sections/*.liquid, snippets/*.liquid, assets/*.css, assets/*.js, layout/theme.liquid Shopify can include non-conflicting edits; changes it cannot carry over need manual migration.
Settings and layout data config/settings_data.json, templates/*.json, section groups Shopify copies theme editor customizations. A manual migration requires checking compatibility with the new ids and schemas.

When manual migration is needed, separate code reconciliation from settings adaptation. Review the updated copy before publishing, even when Shopify reports that it included the code edits.

If you want Shopify’s own view first, the Help Center explains how theme updates interact with code changes.

Step 0: never work on the live theme

Pull the live theme into a git repository so every change is visible and reversible. With Shopify CLI:

mkdir store-theme && cd store-theme
shopify theme pull --store my-store --live
git init && git add -A && git commit -m "Live theme as customized"

--live (-l) pulls the published theme, and --store accepts the store prefix or the full myshopify.com URL. From here on you work on copies, and the live theme stays untouched.

Step 1: find what was customized

You need two clean, vanilla vendor copies: the exact version underlying the currently published theme (v1) and the new version (v2). v1 is not necessarily the first version the store used. If you cannot obtain that exact base, the diff mixes vendor changes and customizations; do not treat it as a reliable automated migration. Keep the copies beside the live theme:

theme-upgrade/
├── vendor/
│   ├── v1/      # vanilla version matching the published theme
│   └── v2/      # new vanilla version
└── live/        # the customized theme you just pulled

The difference between v1 and live is your customization inventory. Git can compare plain folders:

git diff --no-index --stat vendor/v1 live
git diff --no-index --name-status vendor/v1 live
git diff --no-index --name-status vendor/v1 vendor/v2

The difference between v1 and live identifies files added, modified or deleted through customizations, apps or merchant configuration. Also compare v1 with v2 to identify vendor changes. git diff --no-index returns 1 when differences are found; that does not mean the comparison failed.

Step 2: three-way merge, not copy and paste

A three-way merge uses v1 as the base to combine your changes with vendor changes. It does not guarantee a working result: conflicts and architectural changes require manual review. Git documents the arguments and exit codes of git merge-file.

git merge-file does exactly that. Its arguments are your file, the base, and the incoming file:

git merge-file <current> <base> <incoming>
# <current>  = your customized file (it is overwritten with the result)
# <base>     = vendor/v1 version
# <incoming> = vendor/v2 version

This example prepares a fresh copy and merges only one reviewed Liquid file, present in all three versions. Replace file with a file from your inventory. The upgrade directory must not exist; otherwise the script stops to avoid overwriting it or creating nested folders.

#!/usr/bin/env bash
set -eu
file='sections/main-product.liquid'

if [ -e upgrade ] || [ -L upgrade ]; then
  echo "upgrade already exists; use a fresh directory." >&2
  exit 1
fi
for dir in live vendor/v1 vendor/v2; do
  if [ ! -f "$dir/$file" ]; then
    echo "One of the three file versions is missing. $dir/$file" >&2
    exit 1
  fi
done

cp -R vendor/v2 upgrade
cp "live/$file" "upgrade/$file"
if git merge-file "upgrade/$file" "vendor/v1/$file" "vendor/v2/$file"; then
  echo "Merged; review the result. $file"
else
  status=$?
  if [ "$status" -le 127 ]; then
    echo "Conflicts: resolve markers before continuing." >&2
  else
    echo "git merge-file failed; check the files and Git error message." >&2
  fi
  exit "$status"
fi

git merge-file returns 0 for a conflict-free merge, between 1 and 127 for conflicts, and a different error code for other failures. The script exits unsuccessfully in both cases. The <<<<<<<, ======= and >>>>>>> markers require manual resolution; a merge without markers still needs testing.

This example does not migrate the whole theme. Complete the inventory before uploading the result:

  • Reconcile config/settings_data.json, templates/*.json, section groups and other JSON separately, preserving merchant data and checking section types, setting ids and block order against the new theme.
  • Copy added custom files only after checking that the vendor did not introduce a file at the same path.
  • Review files deleted in live: the v2 copy can reintroduce them. Decide whether to keep, remove or adapt their references before publishing.
  • Review files removed or renamed by the vendor and their dependencies. A customization may need to move to another section.
  • Handle binary assets, such as images and fonts, by selection or replacement; do not pass them to git merge-file.

Step 3: isolate what the theme owns

One thing the Charlotte’s Web upgrade required was adapting styles to the fact that the new version changed how global style variables were read. Custom code that depended on those variables had to be adjusted, and that is the kind of change an upgrade can introduce without warning.

You cannot prevent a vendor from renaming things, but you can limit the blast radius. Put every dependency on theme-owned settings in one snippet, and have your custom code depend on your own variables:

{%- comment -%}
  snippets/brand-vars.liquid
  The only file that knows the theme's setting ids.
  After an upgrade, this is the file to fix.
{%- endcomment -%}
<style>
  :root {
    --brand-accent: {{ settings.color_accent | default: '#0a7a5c' }};
    --brand-radius: {{ settings.button_radius | default: 8 }}px;
  }
</style>

color_accent and button_radius are placeholders: setting ids differ from theme to theme, so look them up in the theme’s config/settings_schema.json. Render the snippet once in layout/theme.liquid:

{% render 'brand-vars' %}

A custom section can then use var(--brand-accent) without knowing anything about the theme’s settings. Its own schema stays small and explicit:

<section class="promo-strip">
  <h2>{{ section.settings.heading | escape }}</h2>
</section>

{% schema %}
{
  "name": "Promo strip",
  "settings": [
    {
      "type": "text",
      "id": "heading",
      "label": "Heading",
      "default": "Free shipping this week"
    }
  ],
  "presets": [{ "name": "Promo strip" }]
}
{% endschema %}

Step 4: Tailwind without fighting the theme

If your customizations use Tailwind, keep it from colliding with the theme’s CSS. Tailwind v4 supports a class prefix, and the upgrade guide shows prefixed utilities written like variants, such as tw:flex.

To omit Preflight (Tailwind’s base reset), import only the theme and utilities layers. The Preflight documentation places the prefix on both imports. This example also compiles with Tailwind 4.1.18:

/* tailwind.css */
@layer theme, base, components, utilities;
@import "tailwindcss/theme.css" layer(theme) prefix(tw);
@import "tailwindcss/utilities.css" layer(utilities) prefix(tw);

@source "./sections";
@source "./snippets";
@source "./layout";

Build it into assets/, where Shopify serves it:

npm install tailwindcss @tailwindcss/cli
npx @tailwindcss/cli -i ./tailwind.css -o ./assets/custom-tailwind.css --minify

Load it like any other theme asset and use the prefixed classes in Liquid:

{{ 'custom-tailwind.css' | asset_url | stylesheet_tag }}

<div class="tw:flex tw:items-center tw:gap-4">
  {{ section.settings.heading | escape }}
</div>

The prefix reduces class and variable name collisions. It does not fully isolate your CSS: global theme selectors, the cascade and markup changes can still affect your components. Check them in the preview.

Step 5: test in an unpublished theme

Push the merged result as a new unpublished theme, never over the live one:

cd upgrade
shopify theme check
shopify theme push --store my-store --unpublished --theme "Upgrade 2026-10"

shopify theme check lints Liquid, and --unpublished creates a new theme and, without --development, takes its name from --theme. To iterate with hot reload against that theme:

shopify theme dev --store my-store --theme "Upgrade 2026-10"

Then walk the whole user workflow in the preview, not only the pages you edited:

  • Home, collection, product and cart on desktop and mobile.
  • Every custom section in the theme editor: add it, change its settings, reorder it.
  • Navigation, search, filters, and any form or popup you customized.
  • Language and currency switchers if the store uses them.
  • Apps that inject blocks or embeds.

Open the preview in the theme editor too. A section that renders fine on the storefront can still fail in the editor if its schema changed.

Step 6: publish and keep the rollback

When staging passes, publish from the admin or the CLI. The previous theme stays in the library, so rollback is a single publish away. Keep it there for a few weeks and note the date in the repo.

A short checklist to close the job:

  1. Customization inventory reviewed against vendor/v1.
  2. Added, changed and removed files accounted for.
  3. JSON templates and settings reconciled with the new schemas.
  4. shopify theme check clean.
  5. Full workflow tested in the unpublished theme, in the editor and on the storefront.
  6. Old theme kept as a rollback, and the repo tagged with both versions.

When it is worth getting help

An upgrade is mostly careful reading. It is worth an extra pair of eyes when the store has many custom sections, when the vendor restructured the theme between versions, or when the store cannot afford a bad publish day.

This is the kind of work I do as a Shopify developer in Mexico City, and you can see more of it in the case studies, including a homepage and product page build for Cream Right and collection filters for Exclusive Iron Doors. If you have a theme upgrade ahead, write to me with the store and the theme name.

Loading post views…