Syncing site styles with theme.json
Let’s talk about how to programmatically update the “styles” section in theme.json by taking them from the current site settings. In (FSE theme) WordPress, the main design settings are stored in the theme.json file.
At the same time, part of these parameters can be changed through the WordPress site editor, via the “Styles” section.
The problem is that changes from the editor are not written back into the theme file theme.json. WordPress saves them in the database.
Because of this, the final theme design consists of two parts:
- base styles from
theme.json; - user modifications from the database.
Sometimes it’s necessary to move settings made through the editor back into the theme file. For example, to save them in Git, transfer them to another site, or make them part of a new theme version.
What the code does
The class gets the user styles, merges them with the styles from the current theme.json, and writes the result back to the file.
At the same time, only the styles section is replaced.
The other parts of theme.json, for example settings, remain unchanged. Their original formatting is also preserved.
Code of File: Theme_Json_Styles_Updater.php
GitHub<?php
/**
* Updates the root `styles` section in the theme.json file.
*
* The class retrieves user styles saved through the Site Editor,
* merges them with the styles defined in theme.json, and replaces only
* the `styles` section. Formatting of the other sections remains unchanged.
*
* Usage:
*
* $updater = new Theme_Json_Styles_Updater(
* theme_json_file: get_stylesheet_directory() . '/theme.json',
* indent: ' '
* );
*
* $updater->update();
*
* Version: 1.0
*/
final class Theme_Json_Styles_Updater {
public function __construct(
private readonly string $theme_json_file,
private readonly string $indent = ' '
) {}
public function update(): void {
$theme_json = $this->read_theme_json();
$styles = $this->get_merged_styles( $theme_json );
$new_styles = $this->encode_styles( $styles );
$theme_json = $this->replace_root_object( $theme_json, 'styles', $new_styles );
$this->write_theme_json( $theme_json );
}
private function read_theme_json(): string {
$contents = file_get_contents( $this->theme_json_file );
if ( false === $contents ) {
throw new RuntimeException( 'Unable to read theme.json.' );
}
return $contents;
}
private function get_merged_styles( string $theme_json ): array {
$theme_data = json_decode(
$theme_json,
true,
512,
JSON_THROW_ON_ERROR
);
$user_data = WP_Theme_JSON_Resolver::get_user_data()->get_raw_data();
return array_replace_recursive(
$theme_data['styles'] ?? [],
$user_data['styles'] ?? []
);
}
private function encode_styles( array $styles ): string {
$json = wp_json_encode(
$styles,
JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE
);
if ( ! $json ) {
throw new RuntimeException( 'Unable to encode styles.' );
}
$json = preg_replace_callback(
'/^( +)/m',
fn( $match ) => str_repeat( $this->indent, strlen( $match[1] ) / 4 ),
$json
);
return $this->indent_json( $json );
}
private function indent_json( string $json ): string {
return preg_replace( '/^/m', $this->indent, $json ) ?: $json;
}
private function replace_root_object( string $json, string $property, string $replacement ): string {
$start = $this->find_root_object_start( $json, $property );
$end = $this->find_object_end( $json, $start );
return substr_replace(
$json,
ltrim( $replacement ),
$start,
$end - $start
);
}
private function find_root_object_start( string $json, string $property ): int {
$property_position = strpos( $json, "\"$property\"" );
if ( false === $property_position ) {
throw new RuntimeException( "Property \"$property\" was not found." );
}
$object_start = strpos( $json, '{', $property_position );
if ( false === $object_start ) {
throw new RuntimeException( "Object \"$property\" was not found." );
}
return $object_start;
}
private function find_object_end( string $json, int $object_start ): int {
$depth = 0;
$in_string = false;
$escaped = false;
$length = strlen( $json );
for ( $position = $object_start; $position < $length; $position++ ) {
$character = $json[ $position ];
if ( $in_string ) {
if ( $escaped ) {
$escaped = false;
} elseif ( '\\' === $character ) {
$escaped = true;
} elseif ( '"' === $character ) {
$in_string = false;
}
continue;
}
if ( '"' === $character ) {
$in_string = true;
} elseif ( '{' === $character ) {
$depth++;
} elseif ( '}' === $character && 0 === --$depth ) {
return $position + 1;
}
}
throw new RuntimeException( 'Unable to find the end of the object.' );
}
private function write_theme_json( string $theme_json ): void {
if ( ! file_put_contents( $this->theme_json_file, $theme_json ) ) {
throw new RuntimeException( 'Unable to write theme.json.' );
}
}
}Code of File: usage.php
GitHub<?php $updater = new Theme_Json_Styles_Updater( theme_json_file: get_stylesheet_directory() . '/theme.json', indent: ' ' ); $updater->update();
Now we run the code
We create an instance of the class, pass the path to theme.json, and call the update() method:
$updater = new Theme_Json_Styles_Updater( get_stylesheet_directory() . '/theme.json', ' ' ); $updater->update();
The second argument sets the indentation:
' '- two spaces;"\t"- tab.
Notes
How the update works
First, the class reads the current contents of the theme.json file.
Then, the class gets the user settings via:
WP_Theme_JSON_Resolver::get_user_data()
This method returns the data that WordPress saved after changes in the site editor.
From the retrieved data, only the styles section is used.
The styles from the database are merged with the styles from the file. If there is a match, the value from the database takes priority.
For example, in the file it is specified:
{
"styles": {
"color": {
"text": "#111111",
"background": "#ffffff"
}
}
}
And through the site editor the user changed only the text color:
{
"styles": {
"color": {
"text": "#333333"
}
}
}
After the merge, the result will be:
{
"styles": {
"color": {
"text": "#333333",
"background": "#ffffff"
}
}
}
Why the entire theme.json isn’t overwritten
The simplest option would look like this:
- Read the entire file.
- Convert it with
json_decode(). - Change the
stylessection. - Save the entire file via
json_encode().
But in this case, PHP will completely change the file formatting.
For example, a compact representation:
"margin": { "top": "20px", "bottom": "20px" }
may become:
"margin": {
"top": "20px",
"bottom": "20px"
}
Also, indentation and line-break positions in the entire file may change.
Therefore, the class finds the beginning and end only of the root styles object, and then replaces that fragment in the original string.
So the formatting of the other sections remains untouched.
Important point
After transferring the settings, the user styles will remain in the database.
This means that WordPress will continue applying them on top of theme.json. Usually the result will stay the same, because the same values were written into the file.
But if you then manually change these parameters in theme.json, the saved settings from the database may continue to override them.
Therefore, after a successful transfer, it may be necessary to reset the user styles in the site editor or delete the corresponding wp_global_styles entry.