Read more of this story at Slashdot.
Read more of this story at Slashdot.
Gemini 3 Flash is available for developers to build with now. Learn more about this smarter, scale-ready model and how — and where — you can use it now.
Managing multiple client sites often means juggling local setups, updates, and changes across different environments.
It works — until the workflow starts getting in your way.
Small issues, like inconsistent configurations, overwritten files, and repetitive setup tasks, can all add up and slow you down.
WordPress Studio simplifies all of that.
The free, open-source tool lets you spin up local sites quickly, share previews instantly, and move changes between environments without the usual hassle — helping you focus on creating rather than configuring and troubleshooting.
Here’s how you can use it to manage multiple client WordPress sites.

You have three options for creating a new site in WordPress Studio:
Here are more details on those three options.
Starting with a blank site creates a fresh WordPress installation using the default out-of-the-box configuration.
This option works well for one-off builds, but Studio can save even more time once you start using Blueprints.

Blueprints are reusable JSON files that act as recipes for creating preconfigured local sites — they’re one of the key ways that Studio helps you save time and reduce repetitive tasks.
Instead of setting up each project from scratch, you can create Blueprints for various website types (e.g., blogs or online stores) — defining everything your site needs, from WordPress and PHP versions to themes, plugins, settings, and content.
The Studio Assistant and interactive builder help you generate these automatically — simply tell the AI-powered assistant the site configuration, and it will create the Blueprint.

To use a Blueprint in Studio, choose “Start from a Blueprint” and either pick one of the featured options or upload your own Blueprint file.
Studio currently offers three featured Blueprints (you can preview each in WordPress Playground):
You can also browse the WordPress Blueprints Gallery for community-created configurations.

Note: Sites on the WordPress.com Business and Commerce plans don’t need to be imported from a backup. Instead, they can use the Studio Sync feature. This is more powerful and efficient than importing from a backup file.
You can also import a WordPress site into Studio from a backup file. This is useful if you have an existing site you’d like to work on locally.
Follow these steps to import from a backup file:
Install one of the supported backup plugins, such as the free All-in-One WP Migration and Backup plugin, on the site you’d like to import into Studio.
Then, create a backup of the site (WP Admin → All-in-One WP Migration → Backups → Create Backup).

From here, download the backup file and load it into WordPress Studio.

When you’re done working on the local version, use the plugin to create a new backup and import that backup into the live site.
Studio lets you configure each local site to match the hosted environment, making sure they’re compatible.
The local site’s environment can be configured from the “Advanced settings” panel.

Whether you start with a blank site, a Blueprint, or a backup, Studio lets you adjust a range of optional settings for your local environment. For example, you can:
Tip: You can change these settings after you’ve created a site.

After configuring the environment, you can also set up the tools you want Studio to use while you work.
These settings can be accessed from Settings → Preferences.

Your preferred tools will be used when accessing the site from the “Open in…” section of the Overview tab.

Once your local site is configured, you can begin developing and testing changes.
WordPress Studio applies updates instantly as you work, so you can move quickly and collaborate without delays.
Your local site updates in real time — whether you’re editing files, adjusting settings in WP Admin, or adding plugins and themes.
When you do need to add plugins or themes, you can install them through WP Admin just as you would on a hosted site, or drop the files directly into the site’s folders.
If you use certain plugins or themes regularly, keeping them on your computer makes adding them to each new project even faster.

Tip: If you reuse the same plugins across projects, Blueprints (from Step 1) let you spin up sites preconfigured with your preferred plugins, themes, and settings. Studio’s AI Assistant can also help you make updates to your local sites.

Beyond installing plugins and themes, you can also edit your site’s files directly.
Studio gives you quick access to those files from the Overview tab.

The “Open in…” section gives you quick access to the site’s files and folders.
This is useful if you want to edit a local site’s files, including plugin or theme files, in your preferred code editor.

Each time you edit and save a file, your local site will immediately start using the updated version — there’s no need to wait for files to upload to a server.
Tip: Our blog post on Local WordPress Development Workflows Using WordPress Studio includes a helpful section on the ideal development workflow, whether you’re creating sites, plugins, or themes.

While working on a site, you can also use the preview feature to get client and collaborator feedback.

Previews are a useful addition to any workflow because they help you get more accurate feedback, faster.
This way, your clients get to experience the site for themselves, instead of relying on inefficient screenshots or video walkthroughs.
All you need to do is share the temporary URL with clients and team members, and they can inspect the site snapshot remotely.
The preview feature is powered by WordPress.com and uses a temporary domain (wp.build).

The main aspects of the preview sites feature include:

After building locally, use Studio’s Sync feature to synchronize your local and hosted sites in either direction (push or pull).
The user-friendly interface and ability to selectively sync reduce the risk of accidental overwrites that can happen when transferring files manually.
Tip: Sync is available on WordPress.com Business and Commerce plans. These plans have Jetpack enabled by default, so your hosted site can connect to Studio and use Sync without any extra setup.

You can synchronize between a local site and the hosted production and staging environments.
Synchronizing with the staging site is especially useful as it lets you test your work in a private hosted environment before moving it to the live production site.

As Studio supports selective sync, you can push or pull only the files, folders, or database tables you need.

Thanks to selective sync, it’s easy to push just a theme from your local site to your hosted WordPress.com site and vice versa, leaving the rest of the site intact.
A backup is created when you initiate a sync, so you can restore your site if necessary. An email notification is also sent when the sync completes.
Now it’s time to test the site in a staging environment — a feature available to WordPress.com Business and Commerce plans.
This gives you a safe place to identify issues before they go live.
For the best results, follow one of these workflows after creating a WordPress.com staging site:

You can use the switcher to change between the production and staging environments.

See the WordPress.com documentation to find out how staging sites work.
Now that your staging site is set up, here are two workflows that show how to use WordPress Studio when working on client sites:
This workflow for building a new client site involves creating a local site, sharing a preview, pushing to the staging site, and then pushing to the production site.
Follow these steps:

WordPress.com has a built-in Coming Soon mode with a preview feature that’s useful for controlling access to sites in development.
This workflow lets you update an existing live site without overwriting important content or disrupting anything outside the changes you’ve made.
Selective Sync ensures you don’t overwrite important live content — such as form submissions, comments, orders, or anything added while you were working.
For this scenario:

The live site now includes your theme changes, and any other updates made while you were working locally won’t be overwritten.
Once your workflow is in place, WordPress Studio makes it easy to scale your process across multiple client projects.
Instead of repeating setup work or jumping between disconnected tools, you can reuse configurations, switch between projects instantly, and keep each site organized and isolated.
Use Studio’s core actions to stay efficient as your client list grows:
WordPress Studio is a fast, open-source, and free way to build and manage local WordPress sites.
It helps you save time, share work with clients more effectively, and reduce errors when transferring files.
Blueprints let you spin up consistent, pre-configured sites in seconds, reducing setup time and repetitive work — so you can receive and apply client feedback with ease.
If you’re using WordPress.com’s Business or Commerce plans, Sync adds an extra layer by letting you move work between local, staging, and production safely and with confidence.
The bottom line: No matter where the final site is hosted, Studio helps you manage multiple client projects with less overhead and more control.

Ready for the second part? If you recall, last time we worked on a responsive list of overlapping avatar images featuring a cut-out between them.

We are still creating a responsive list of avatars, but this time it will be a circular list.

This design is less common than the horizontal list, but it’s still a good exercise to explore new CSS tricks.
Let’s start with a demo. You can resize it and see how the images behave, and also hover them to get a cool reveal effect.
The following demo is currently limited to Chrome and Edge, but will work in other browsers as the sibling-index() and sibling-count() functions gain broader support. You can track Firefox support in Ticket #1953973 and WebKit’s position in Issue #471.
We will rely on the same HTML structure and CSS base as the example we covered in Part 1: a list of images inside a container with mask-ed cutouts. This time, however, the positions will be different.
There are several techniques for placing images around a circle. I will start with my favorite one, which is less known but uses a simple code that relies on the CSS offset property.
.container {
display: grid;
}
.container img {
grid-area: 1/1;
offset: circle(180px) calc(100%*sibling-index()/sibling-count()) 0deg;
}
The code doesn’t look super intuitive, but its logic is fairly straightforward. The offset property is a shorthand, so let’s write it the longhand way to see how breaks down:
offset-path: circle(180px);
offset-distance: calc(100%*sibling-index()/sibling-count());
offset-rotate: 0deg;
We define a path to be a circle with a radius of 180px. All the images will “follow” that path, but will initially be on top of each other. We need to adjust their distance to change their position along the path (i.e., the circle). That’s where offset-distance comes into play, which we combine with the sibling-index() and sibling-count() functions to create code that works with any number of elements instead of working with exact numbers.
For six elements, the values will be as follows:
100% x 1/6 = 16.67%
100% x 2/6 = 33.33%
100% x 3/6 = 50%
100% x 4/6 = 66,67%
100% x 5/6 = 83.33%
100% x 6/6 = 100%
This will place the elements evenly around the circle. To this, we add a rotation equal to 0deg using offset-rotate to keep the elements straight so they don’t rotate as they follow the circular path. From there, all we have to do is update the circle’s radius with the value we want.
That’s my preferred approach, but there is a second one that uses the transform property to combine two rotations with a translation:
.container {
display: grid;
}
.container img {
grid-area: 1/1;
--_i: calc(1turn*sibling-index()/sibling-count());
transform: rotate(calc(-1*var(--_i))) translate(180px) rotate(var(--_i));
}
The translation contains the circle radius value and the rotations use generic code that relies on the sibling-* functions the same way we did with offset-distance.
Even though I prefer the first approach, I will rely on the second one because it allows me to reuse the rotation angle in more places.
Similar to the horizontal responsive list from the last article, I will rely on container query units to define the radius of the circle and make the component responsive.

.container {
--s: 120px; /* image size */
aspect-ratio: 1;
container-type: inline-size;
}
.container img {
width: var(--s);
--_r: calc(50cqw - var(--s)/2);
--_i: calc(1turn*sibling-index()/sibling-count());
transform: rotate(calc(-1*var(--_i))) translate(var(--_r)) rotate(var(--_i));
}
Resize the container in the demo below and see how the images behave:
It’s responsive, but when the container gets bigger, the images are too spread out, and I don’t like that. It would be good to keep them as close as possible. In other words, we consider the smallest circle that contains all the images without overlap.
Remember what we did in the first part: we added a maximum boundary to the margin for a similar reason. We will do the same thing here:
--_r: min(50cqw - var(--s)/2, R);
I know you don’t want a boring geometry lesson, so I will skip it and give you the value of R:
S/(2 x sin(.5turn/N))
Written in CSS:
--_r: min(50cqw - var(--s)/2,var(--s)/(2*sin(.5turn/sibling-count())));
Now, when you make the container bigger, the images will stay close to each other, which is perfect:
Let’s introduce another variable for the gap between images (--g) and update the formula slightly to keep a small gap between the images.
.container {
--s: 120px; /* image size */
--g: 10px; /* the gap */
aspect-ratio: 1;
container-type: inline-size;
}
.container img {
width: var(--s);
--_r: min(50cqw - var(--s)/2,(var(--s) + var(--g))/(2*sin(.5turn/sibling-count())));
--_i: calc(1turn*sibling-index()/sibling-count());
transform: rotate(calc(-1*var(--_i))) translate(var(--_r)) rotate(var(--_i));
}
For this part, we will be using the same mask that we used in the last article:
mask: radial-gradient(50% 50% at X Y, #0000 calc(100% + var(--g)), #000);
With the horizontal list, the values of X and Y were quite simple. We didn’t have to define Y since its default value did the job, and the X value was either 150% + M or -50% - M, with M being the margin that controls the overlap. Seen differently, X and Y are the coordinates of the center point of the next or previous image in the list.
That’s still the case this time around, but the value is trickier to calculate:

The idea is to start from the center of the current image (50% 50%) and move to the center of the next image (X and Y). I will first follow segment A to reach the center of the big circle and then follow segment B to reach the center of the next image.
This is the formula:
X = 50% - Ax + Bx
Y = 50% - Ay + By
Ax and Ay are the projections of the segment A on the X-axis and the Y-axis. We can use trigonometric functions to get the values.
Ax = r x sin(i);
Ay = r x cos(i);
The r represents the circle’s radius defined by the CSS variable --_r, and i represents the angle of rotation defined by the CSS variable --_i.
Same logic with the B segment:
Bx = r x sin(j);
By = r x cos(j);
The j is similar to i, but for the next image in the sequence, meaning we increment the index by 1. That gives us the following CSS calculations for each variable:
--_i: calc(1turn*sibling-index()/sibling-count());
--_j: calc(1turn*(sibling-index() + 1)/sibling-count());
And the final code with the mask:
.container {
--s: 120px; /* image size */
--g: 14px; /* the gap */
aspect-ratio: 1;
container-type: inline-size;
}
.container img {
width: var(--s);
--_r: min(50cqw - var(--s)/2,(var(--s) + var(--g))/(2*sin(.5turn/sibling-count())));
--_i: calc(1turn*sibling-index()/sibling-count());
--_j: calc(1turn*(sibling-index() + 1)/sibling-count());
transform: rotate(calc(-1*var(--_i))) translate(var(--_r)) rotate(var(--_i));
mask: radial-gradient(50% 50% at
calc(50% + var(--_r)*(cos(var(--_j)) - cos(var(--_i))))
calc(50% + var(--_r)*(sin(var(--_i)) - sin(var(--_j)))),
#0000 calc(100% + var(--g)), #000);
}
Cool, right? You might notice two different implementations for the cut-out. The formula I used previously considered the next image, but if we consider the previous image instead, the cut-out goes in another direction. So, rather than incrementing the index, we decrement instead and assign it to a .reverse class that we can use when we want the cut-out to go in the opposite direction:
.container img {
--_j: calc(1turn*(sibling-index() + 1)/sibling-count());
}
.container.reverse img {
--_j: calc(1turn*(sibling-index() - 1)/sibling-count());
}
Similar to what we did in the last article, the goal of this animation is to remove the overlap when an image is hovered to fully reveal it. In the horizontal list, we simply set its margin property to 0, and we adjust the margin of the other images to prevent overflow.
This time, the logic is different. We will rotate all of the images except the hovered one until the hovered image is fully visible. The direction of the rotation will depend on the cut-out direction, of course.

To rotate the image, we need to update the --_i variable, which is used as an argument for the rotate function. Let’s start with an arbitrary value for the rotation, say 20deg.
.container img {
--_i: calc(1turn*sibling-index()/sibling-count());
}
.container:has(:hover) img {
--_i: calc(1turn*sibling-index()/sibling-count() + 20deg);
}
.container.reverse:has(:hover) img {
--_i: calc(1turn*sibling-index()/sibling-count() - 20deg);
}
Now, when an image is hovered, all of images rotate by 20deg. Try it out in the following demo.
Hmm, the images do indeed rotate, but the mask is not following along. Don’t forget that the mask considers the position of the next or previous image defined by --_j and the next/previous image is rotating — hence we need to also update the --_j variable when the hover happens.
.container img {
--_i: calc(1turn*sibling-index()/sibling-count());
--_j: calc(1turn*(sibling-index() + 1)/sibling-count());
}
.container.reverse img {
--_j: calc(1turn*(sibling-index() - 1)/sibling-count());
}
.container:has(:hover) img {
--_i: calc(1turn*sibling-index()/sibling-count() + 20deg);
--_j: calc(1turn*(sibling-index() + 1)/sibling-count() + 20deg);
}
.container.reverse:has(:hover) img {
--_i: calc(1turn*sibling-index()/sibling-count() - 20deg);
--_j: calc(1turn*(sibling-index() - 1)/sibling-count() - 20deg);
}
That’s a lot of redundant code. Let’s optimize it a little by defining additional variables:
.container img {
--_a: 20deg;
--_i: calc(1turn*sibling-index()/sibling-count() + var(--_ii, 0deg));
--_j: calc(1turn*(sibling-index() + 1)/sibling-count() + var(--_jj, 0deg));
}
.container.reverse img {
--_i: calc(1turn*sibling-index()/sibling-count() - var(--_ii, 0deg));
--_j: calc(1turn*(sibling-index() - 1)/sibling-count() - var(--_jj, 0deg));
}
.container:has(:hover) img {
--_ii: var(--_a);
--_jj: var(--_a);
}
Now the angle (--_a) is defined in one place, and I consider two intermediate variables to add an offset to the --_i and --_j variables.
The rotation of all the images is now perfect. Let’s disable the rotation of the hovered image:
.container img:hover {
--_ii: 0deg;
--_jj: 0deg;
}
Oops, the mask is off again! Do you see the issue?
We want to stop the hovered image from rotating while allowing the rest of the images to rotate. Therefore, the --_j variable of the hovered image needs to update since it’s linked to the next or previous image. So we should remove --_jj: 0deg and keep only --_ii: 0deg.
.container img:hover {
--_ii: 0deg;
}
That’s a little better. We fixed the cut-out effect on the hovered image, but the overall effect is still not perfect. Let’s not forget that the hovered image is either the next or previous image of another image, and since it’s not rotating, another --_j variable needs to remain unchanged.
For the first list, it’s the variable of the previous image that should remain unchanged. For the second list, it’s the variable of the next image:
/* select previous element of hovered */
.container:not(.reverse) img:has(+ :hover),
/* select next element of hovered */
.container.reverse img:hover + * {
--_jj: 0deg;
}
In case you are wondering how I knew to do this, well, I tried both ways and I picked the one that worked. It was either the code above or this:
.container:not(.reverse) img:hover + *,
.container.reverse img:has(+ :hover) {
--_jj: 0deg;
}
We are getting closer! All the images behave correctly except for one in each list. Try hovering all of them to identify the culprit.
Can you figure out what we are missing? Think a moment about it.
Our list is circular, but the HTML code is not, so even if the first and last images are visually placed next to each other, in the code, they are not. We cannot link both of them using the adjacent sibling selector (+). We need two more selectors to cover those edge cases:
.container.reverse:has(:last-child:hover) img:first-child,
.container:not(.reverse):has(:first-child:hover) img:last-child {
--_jj: 0deg;
}
Oof! We have fixed all the issues, and now our hover effect is great, but it’s still not perfect. Now, instead of using an arbitrary value for the rotation, we need to be accurate. We have to find the smallest value that removes the overlap while keeping the images as close as possible.

We can get the value with some trigonometry. I’ll skip the geometry lesson again (we have enough headaches as it is!) and give you the value:
--_a: calc(2*asin((var(--s) + var(--g))/(2*var(--_r))) - 1turn/sibling-count());
Now we can say everything is perfect!
This one was a bit tough, right? Don’t worry if you got a bit lost with all the complex formulas. They are very specific to this example, so even if you have already forget about them, that’s fine. The goal was to explore some modern features and a few CSS tricks such as offset, mask, sibling-* functions, container query units, min()/max(), and more!
Responsive List of Avatars Using Modern CSS (Part 2) originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.