---
title: "Custom Code in Elementor Without a Child Theme: What Survives an Update and What Does Not"
url: https://adityaarsharma.com/elementor-custom-code-without-child-theme/
date: 2026-09-17
modified: 2026-09-03
author: "Aditya Sharma"
description: "A theme update deletes the theme directory before copying the new one. That is the mechanism. Here are the five places code can live, and what each survives."
categories:
  - "Elementor"
  - "WordPress"
image: https://adityaarsharma.com/wp-content/uploads/2026/09/897316cc-92cd-4308-8b4c-89bfda607f19_2912x1632-1024x574.webp
word_count: 1863
---

# Custom Code in Elementor Without a Child Theme: What Survives an Update and What Does Not

This is the line that eats people's custom code. In WordPress 7.1, `Theme_Upgrader::upgrade()` adds `delete_old_theme` to the `upgrader_clear_destination` filter and calls `run()` with `clear_destination` set to true.

![A WordPress theme update does not merge. It deletes the theme directory, then copies the new one in.](https://adityaarsharma.com/wp-content/uploads/2026/09/897316cc-92cd-4308-8b4c-89bfda607f19_2912x1632-scaled.png)
The theme directory is deleted, then the new one is copied in. Not merged. Deleted.

`Plugin_Upgrader::upgrade()` does the same thing with `delete_old_plugin`. Whatever you typed into a parent theme's `functions.php` is gone the next time that theme updates, and there is no warning.

The standard answer is a child theme. That is one option out of five, and for most of what people actually put in `functions.php` on an Elementor site, it is not the best one.

Here is what each location really stores, and what survives what.

On this page

- What JFIF actually is- Why Windows and Chrome keep producing them- The one minute fix: rename it- When a rename is not enough- Converting it with Grabio- What to do with a folder of them- The thing not to do- While you are at it- Questions people ask

## The five places, and what each one survives

| Where you put it | Actually stored in | Survives theme update | Survives theme switch | Survives plugin update |
| ---------------- | ------------------ | --------------------- | --------------------- | ---------------------- |
| Parent theme `functions.php` | a file in the theme directory | **no** | no | yes |
| Child theme `functions.php` | a file in the child theme directory | yes | no | yes |
| A must-use plugin | `wp-content/mu-plugins/*.php` | yes | yes | yes |
| Customizer Additional CSS | a `custom_css` post keyed to the theme slug | yes | **no** | yes |
| Elementor Custom CSS or Custom Code | post meta and a custom post type | yes | yes | yes |

Two rows in that table surprise people, so both are worth the mechanism.

### Customizer Additional CSS is keyed to the theme, not to the site

`wp_get_custom_css_post()` in `wp-includes/theme.php` queries for a single post of type `custom_css` whose `name` is `sanitize_title( $stylesheet )`, across every post status.

So it is a real post whose `post_name` is the sanitised stylesheet directory name of the active theme. Update the theme and the post is untouched, because it lives in the database rather than the theme folder.

Switch the theme and the CSS is still in your database and no longer loaded, because the lookup now asks for a different `post_name`.

This catches people at exactly the wrong moment. Activating a child theme is a theme switch.

Every rule you put in Additional CSS while the parent was active stops applying the second you activate the child. Nothing was deleted, and every diagnostic you run will say the CSS is fine.

### Must-use plugins are the location with no update path at all

`wp-settings.php` line 505 loops `wp_get_mu_plugins()` and `include_once`s every file it returns. That is the whole loading mechanism.

`wp_get_mu_plugins()` in `wp-includes/load.php` is deliberately simple. It reads the directory with `readdir()`, keeps anything ending in `.php`, and sorts the result.

Read what that does and does not do. It reads the top level of `wp-content/mu-plugins` only, so **a file inside a subdirectory is never loaded**. It takes `.php` files only.

It sorts alphabetically, which is your only control over load order. It runs before regular plugins.

And no updater in WordPress ever writes to that directory, which is the whole point: there is no code path that can delete your file.

The tradeoffs are real. A must-use plugin has no activation hook, cannot be deactivated from the admin, and does not appear in the normal plugins list.

That last one is a feature when a client is the person clicking Deactivate, and a problem when you are debugging a white screen at midnight. Name the file so future-you finds it: `00-site-fixes.php` rather than `custom.php`.

## What Elementor actually stores where

### Custom CSS, at three levels, all in the database

Elementor Pro puts Custom CSS at three levels, and all three land in the database rather than in a file you maintain:

| Level | Where you enter it | Stored in |
| ----- | ------------------ | --------- |
| Site | Site Settings, Custom CSS tab | the kit document's settings, compiled into `post-<kitID>.css` |
| Page | Page Settings, Advanced | `_elementor_page_settings` post meta |
| Element | the element's Advanced tab | inside the element's node in `_elementor_data` |

The meta keys are declared in `core/base/document.php` as `PAGE_META_KEY = '_elementor_page_settings'` and `ELEMENTOR_DATA_META_KEY = '_elementor_data'`.

All of it compiles into the generated stylesheets under `wp-content/uploads/elementor/css/`, which means clearing Elementor's cache deletes the compiled output and never the source.

If your custom CSS disappeared after a cache clear, it did not. The file just has not been rebuilt yet, and loading the page in the editor once rebuilds it.

### Custom Code is a post type, not a file

Elementor's Custom Code feature is a separate thing again: a post type with its own publish state, scheduling and display conditions, holding HTML, JavaScript and CSS to be injected at `<head>`, body start or body end, with a priority from 1 to 10 where lower runs earlier.

The documentation is unambiguous about its one hard limit:

> Custom code does not support PHP snippets. You cannot use this feature to add custom hooks or actions. You can use a different approach for PHP, either through functions.php or a third-party plugin.
>
> Elementor, Add custom code, elementor.com/help/custom-code-pro/, last updated 14 January 2024
![Elementor documentation page for the Custom Code feature, which states that custom code does not support PHP snippets.](https://adityaarsharma.com/wp-content/uploads/2026/09/6dad32c4-0c61-4b69-b1f9-ebe90d98f05a_2800x2100-scaled.png)elementor.com/help/custom-code-pro/, screenshot taken 3 September 2026.
So the split is clean. Anything that is markup, script or style can live in Elementor and travels with the database. Anything that is a WordPress hook cannot, and needs one of the file-based locations above.

![Elementor documentation page Use selector in the custom CSS tab, last updated January 14 2024.](https://adityaarsharma.com/wp-content/uploads/2026/09/8c40871f-9d68-464a-b809-90a5c0663129_2800x2100-scaled.png)elementor.com/help/how-to-use-selector-in-the-custom-css-tab/, screenshot taken 3 September 2026. The Last Update line reads January 14, 2024.

## The selector keyword, and why your CSS targets the wrong element

Elementor's element-level Custom CSS gives you a `selector` keyword. It does not mean the widget. From Elementor's own documentation on it, last updated 14 January 2024, with an Image widget selected:

`selector { border: 5px solid red; } /* borders the wrapper, not the image */
selector img { border: 5px solid red; } /* borders the image */`

### selector is the wrapper, not your content

This is the single most common reason a rule looks correct and does nothing visible. `selector` resolves to the element's wrapper. Your content sits inside it.

The same documentation notes that if you use your own class instead of `selector`, you often need `!important` to beat Elementor's generated rule, which is a specificity fight you can avoid entirely by loading your CSS after Elementor's.

### The two hooks that decide the cascade

From `includes/frontend.php` in the Elementor source, three actions fire in this order:

- `elementor/frontend/before_enqueue_styles`- `elementor/frontend/after_enqueue_styles`, after elementor-icons and elementor-frontend are enqueued- `elementor/frontend/after_enqueue_post_styles`, after `post-<ID>.css` for the current document
Enqueue on `elementor/frontend/after_enqueue_styles` and you land after Elementor's base stylesheet but before the per-post generated CSS.

Enqueue on `elementor/frontend/after_enqueue_post_styles` and you land after both, which is where you want to be if you are overriding values a client set in the editor.

Correct source order beats `!important` every time, and it is the reason [a media query in WordPress refuses to take effect](https://adityaarsharma.com/how-to-fix-media-query-not-working-in-wordpress/) more often than any breakpoint mistake.

Newsletter

## Agents in Production

I check the things our industry takes on trust and publish what I actually found, including when it makes my own work look worse. One researched piece a week.

Email address

Get it weekly

Free. One email a week. Unsubscribe in one click, and I do not send anything else.

## The setup I would actually ship

One file, in `wp-content/mu-plugins/`, holding both the PHP that Elementor cannot host and the enqueue for a stylesheet you own. It survives every theme update, every plugin update, and a theme switch.

`add_action( 'elementor/frontend/after_enqueue_post_styles', function () {
$file = WP_CONTENT_DIR . '/site-custom/site.css';
if ( ! file_exists( $file ) ) { return; }
wp_enqueue_style( 'site-custom', content_url( '/site-custom/site.css' ),
array(), filemtime( $file ) );
} );`
That is the whole file.

Give it a normal plugin header so it shows up in the Must-Use list. Add a second `wp_enqueue_scripts` callback at priority 999 for pages Elementor does not render, guarded with `did_action()` so it never enqueues twice.

### Three details in that file that matter

`filemtime()` as the version argument means the browser refetches the moment you edit the file and never otherwise, which removes the entire class of I-cleared-the-cache-and-it-still-shows-the-old-CSS problem.

The stylesheet lives in `wp-content/site-custom/` rather than in a theme, so it is not in anything's delete path. And the `did_action()` guard stops the file being enqueued twice on a page that Elementor does render.

PHP hooks go in the same file. A settings link after install, a search placeholder change, an email template condition: all of them are five to twenty lines that belong here rather than in a theme.

The pattern in [adding a Go to Settings link after installing a WordPress plugin](https://adityaarsharma.com/go-to-plugin-settings-link-after-install/) and in [changing the WordPress search placeholder without a plugin](https://adityaarsharma.com/how-to-change-search-placeholder-text-in-wordpress/) both drop straight into a must-use plugin unchanged.

## What actually breaks on an Elementor update, and why

### What breaks is CSS written against internal markup

Not your PHP. Elementor's PHP hooks are stable and deprecations are announced. What breaks is CSS written against Elementor's internal markup, because that markup is versioned and it has been shrinking on purpose.

| Removed in | Class you can no longer select |
| ---------- | ------------------------------ |
| Elementor 3.0 | `.elementor-inner`, `.elementor-row`, `.elementor-column-wrap` |
| Elementor 3.2 | `.elementor-image`, `.elementor-text-editor` |
| Elementor 3.6 | `elementor-section-wrap` |
| Elementor 3.19 | all of the above stopped being optional and became core behaviour |

Source: Elementor's Optimized DOM Output documentation, last updated 1 February 2024.

There is a second wave coming from the `e_optimized_markup` feature, whose description in the source says plainly that it includes markup changes so it might require updating custom CSS or JS and cause compatibility issues with third-party plugins.

It is stable, it is off by default on existing sites, and it is on by default for sites installed on 3.30.0 or later.

Which means a site you built last year and a site you build this month behave differently out of the box.

### Find your own exposure in one command

Do it before an update, not after:

`grep -rn --include='*.css' --include='*.php' --include='*.js' \
-e 'elementor-inner' \
-e 'elementor-row' \
-e 'elementor-column-wrap' \
-e 'elementor-section-wrap' \
-e 'elementor-text-editor' \
wp-content/themes wp-content/mu-plugins wp-content/site-custom 2>/dev/null`
Anything that returns is already broken or one release away from it.

Rewrite those selectors against a CSS class you set yourself on the element, in the Advanced tab, so the selector is yours and no Elementor release can remove it.

That is also the durable way to handle things like [custom link colours in Elementor](https://adityaarsharma.com/how-to-easily-add-custom-link-colors-in-elementor/), where the tempting shortcut is to select a generated class.

![Stat card: five Elementor releases removed a class that custom CSS may be selecting.](https://adityaarsharma.com/wp-content/uploads/2026/09/c3f471b5-71ba-437c-9376-09a2ba37b093_2400x2400.png)Built from the removal table in this post. Source: Elementor Optimized DOM Output documentation, last updated 1 February 2024.

## So do you need a child theme

For custom code on an Elementor site, mostly no. A child theme protects files in a theme directory, and on an Elementor build there is usually nothing in the theme directory worth protecting, because Theme Builder has replaced the template files.

A must-use plugin covers the PHP with strictly stronger guarantees, since it also survives a theme switch.

### When a child theme still earns its place

A child theme still earns its place when you are genuinely overriding template files, changing theme supports, or working on a classic theme you intend to keep.

That decision is its own question, and worth reading properly in [whether you need a child theme with Elementor](https://adityaarsharma.com/do-you-need-a-child-theme-with-elementor/), [how a child theme differs from a normal WordPress theme](https://adityaarsharma.com/child-theme-vs-normal-wordpress-theme/), and [whether to create a child theme manually or with a plugin](https://adityaarsharma.com/wordpress-child-theme-manual-creation-vs-plugin/).

Run the grep above against your current project. If it returns nothing, move your snippets out of `functions.php` into a must-use plugin this week, while nothing is on fire.

## Resources

- [class-theme-upgrader.php at the WordPress 7.1 tag](https://github.com/WordPress/wordpress-develop/blob/7.1/src/wp-admin/includes/class-theme-upgrader.php) (clear_destination and delete_old_theme)- [wp-includes/load.php at the WordPress 7.1 tag](https://github.com/WordPress/wordpress-develop/blob/7.1/src/wp-includes/load.php) (wp_get_mu_plugins)- [wp_get_custom_css_post() in the WordPress developer reference](https://developer.wordpress.org/reference/functions/wp_get_custom_css_post/)- [Use selector in the custom CSS tab](https://elementor.com/help/how-to-use-selector-in-the-custom-css-tab/) (Elementor, last updated 14 January 2024)- [Add custom code](https://elementor.com/help/custom-code-pro/) (Elementor, last updated 14 January 2024)- [Custom CSS in Elementor](https://elementor.com/help/custom-css-in-elementor/) (Elementor, last updated 14 January 2024)- [Clear Files and Data](https://elementor.com/help/regenerate-css-data/) (Elementor, last updated 20 January 2026)- [Elementor frontend.php source on GitHub](https://github.com/elementor/elementor/blob/main/includes/frontend.php) (the enqueue hook order)

## More on elementor

- [Why Elementor Blocks SVG Uploads, and the Security Tradeoff You Are Actually Making](https://adityaarsharma.com/elementor-svg-upload-security-tradeoff/)- [Elementor Performance: What Actually Costs You, Measured](https://adityaarsharma.com/elementor-performance-what-actually-costs-you/)- [Elementor vs Gutenberg in 2026: An Honest Read for People Who Ship](https://adityaarsharma.com/elementor-vs-gutenberg-2026/)