Install
This is the repository archive. It installs, but the folder name will carry the branch suffix and updates will not flow.
Install straight from GitHub with WP-CLI:
wp plugin install https://github.com/maithemewp/mai-image-compare/archive/refs/heads/main.zipA before/after image comparison block. Two images, one draggable divider.
One block, no admin menu, no settings page, no database options. Site-wide defaults are set in code through a filter, so they travel with your deploy and show up in a diff.
Requirements
- WordPress 7.0 or newer
- PHP 8.2 or newer
Install
Download the plugin and activate it. Updates come from GitHub through the built-in update checker.
Building from a clone:
- Install the PHP dependencies.
composer install - Install the JavaScript dependencies.
npm install - Build the assets.
npm run build
Using the block
Insert Mai Image Compare, then pick two images. The block sidebar has a Before field and an After field, each shaped like the post's featured image control: click to open the Media Library, then Replace or Remove.
Before is the image on the left, or on top in vertical mode. After is the other one.
Labels are optional badges drawn over each image, and each one only shows while its own image is showing. They are not the Media Library's caption field, which is a separate thing this block does not use.
Alt text lives in a collapsed Image alt text panel at the bottom of the sidebar, because most of the time you do not need it. Each image already uses the alt text from the Media Library. The override is there for the case core's Image block solves the same way: the Media Library's alt text is global, so changing it changes every post using that image, and the right description sometimes depends on the page.
Block attributes
| Attribute | Type | Default | What it does |
|---|---|---|---|
beforeId |
number | none | Attachment ID for the before image. |
afterId |
number | none | Attachment ID for the after image. |
beforeAlt |
string | "" |
Alt text for the before image. Empty means use the Media Library's own alt text. |
afterAlt |
string | "" |
Alt text for the after image. Empty means use the Media Library's own alt text. |
beforeLabel |
string | "" |
Optional badge shown over the before image. Empty means no label. |
afterLabel |
string | "" |
Optional badge shown over the after image. Empty means no label. |
direction |
string | horizontal |
horizontal or vertical. |
value |
number | unset | Start position, 0 to 100. Unset means follow the site default. |
hover |
boolean | unset | Slide on mouse over, with no clicking. Unset means follow the site default. |
dragAnywhere |
boolean | unset | Drag from anywhere in the image, not just the handle. Unset means follow the site default. |
The block also supports wide and full alignment, a custom class name, margin and padding, and WordPress's own aspect ratio control.
Why three attributes have no default
value, hover and dragAnywhere start out with no value at all, and an untouched control writes nothing into the post. That is what lets a later change to the filter reach blocks that were saved months ago.
Saving a page never pins one of these to a value. Clearing one in the editor, through the Reset or "use site default" affordance, removes it again.
The block is server-rendered for the same reason. A static block bakes its attributes into the post content at save time, so a filter change would never reach anything already published.
Site-wide defaults
Three settings are set once per site, in code:
add_filter( 'mai_image_compare_defaults', function( array $defaults ): array {
$defaults['value'] = 25; // Divider starts at 25%.
$defaults['hover'] = true; // Slide on mouse over.
$defaults['dragAnywhere'] = false; // Only the handle moves the divider.
return $defaults;
} );
| Key | Type | Built-in | What it does |
|---|---|---|---|
value |
int | 50 |
Where the divider starts, 0 to 100. Values outside that range are clamped. |
hover |
bool | false |
Slide as the mouse moves over the block, with no click. |
dragAnywhere |
bool | true |
Drag from anywhere in the image. Turn it off and only a drag that starts on the handle moves the divider. |
A block can still deviate. The filter sets what a block inherits, and a block that sets one of the three uses its own value instead. An explicit "off" on a block is a real choice, so a filter turning that setting on will not reach it.
Change the filter and every block that left the control alone follows on the next page view. Nothing needs re-saving.
How the two drag settings relate
They answer different questions. Slide on hover asks whether you have to press the mouse button at all. Drag anywhere asks where you have to grab.
| Slide on hover | Drag anywhere | What it feels like |
|---|---|---|
| off | on | Press and hold anywhere, then drag. This is the default. |
| off | off | Press and hold the handle. Dragging the image does nothing. |
| on | on or off | The divider follows the pointer. No clicking. |
Turning slide on hover on makes drag anywhere irrelevant, because there is no grabbing left to restrict. The editor greys the Drag anywhere control out while that is the case and says why, rather than leaving a control that quietly does nothing. That applies whether hover was turned on for the block or by the filter.
One note for anyone reading the markup: the underlying component asks the opposite question through a handle attribute, so dragAnywhere on renders as handle="false". The inversion happens in one place, where the block renders.
There is deliberately no setting for keyboard control. It is always on.
CSS custom properties
Set these on :root, on .wp-block-mai-image-compare-compare, or on your own class.
| Property | Default | What it does |
|---|---|---|
--mai-image-compare-ratio |
the before image's shape | The block's aspect ratio. Set by the block; override it only if you want every comparison on the site locked to one shape. |
--mai-image-compare-divider-width |
2px |
Width of the line between the two images. |
--mai-image-compare-divider-color |
#fff |
Colour of that line. |
--mai-image-compare-divider-shadow |
0 0 4px rgb( 0 0 0 / 40% ) |
Shadow cast by that line. |
--mai-image-compare-handle-size |
48px |
Width and height of the handle. |
--mai-image-compare-handle-radius |
100vw |
Corner radius of the handle. The default makes it a circle. |
--mai-image-compare-handle-border-width |
2px |
Width of the handle's border. |
--mai-image-compare-handle-border-color |
#fff |
Colour of the handle's border. |
--mai-image-compare-handle-background |
transparent |
Handle background. See-through by default, so the photo shows in the middle. |
--mai-image-compare-handle-shadow |
0 0 6px rgb( 0 0 0 / 35% ) |
Shadow cast by the handle. |
--mai-image-compare-handle-color |
#fff |
Colour of the arrows. |
--mai-image-compare-handle-arrow-width |
2px |
Stroke width of the arrows. |
--mai-image-compare-label-offset |
1rem |
Distance from the label to the nearest two edges. |
--mai-image-compare-label-padding |
0.25em 0.75em |
Padding inside the label. |
--mai-image-compare-label-radius |
3px |
Corner radius of the label. |
--mai-image-compare-label-background |
rgb( 0 0 0 / 70% ) |
Label background. |
--mai-image-compare-label-color |
#fff |
Label text colour. |
--mai-image-compare-label-font-size |
0.8rem |
Label text size. |
Example:
:root {
--mai-image-compare-divider-color: #ffd400;
--mai-image-compare-handle-border-color: #ffd400;
--mai-image-compare-handle-background: #ffd400;
--mai-image-compare-handle-color: #111;
--mai-image-compare-label-background: rgb( 255 212 0 / 90% );
--mai-image-compare-label-color: #111;
}
For anything the properties don't cover, target .mai-image-compare__handle (the circle) and .mai-image-compare__arrows (the arrow SVG) directly. The handle is ordinary page markup, so normal CSS reaches it.
Changing what's inside the handle
The mai_image_compare_handle filter swaps the arrows for your own markup. The circle stays, so the custom properties above still style it. The editor preview uses the same markup.
/**
* Replaces the image compare arrows with the word "Slide".
*
* @param string $html The handle markup. Two arrows by default.
*
* @return string
*/
add_filter( 'mai_image_compare_handle', function( string $html ): string {
return '<span class="my-handle-text">Slide</span>';
} );
The markup is output as is, so it can include an SVG. It isn't escaped, so don't build it from user input.
Images of different sizes
The block picks one shape and both images fill it, cropped from the centre rather than letterboxed. Bars around a comparison read as a bug to a reader, where a crop reads as a decision.
The shape is the before image's own, unless you set one with the block's aspect ratio control, which wins.
The shape is worked out on the server from the attachment's stored dimensions, so the space is the right size before a single image byte downloads. The page does not shift as the images arrive.
Accessibility
The slider takes keyboard focus and announces itself as a slider, with its current position read out as it moves.
| Key | What it does |
|---|---|
| Left and right arrows | Move the divider. |
| Up and down arrows | Move the divider, in vertical mode only. In horizontal mode they scroll the page as usual. |
| Home | Jump to one end. |
| End | Jump to the other end. |
The underlying component ships neither the ARIA nor the Home, End and up/down keys, so this plugin adds them. Keyboard control cannot be switched off, by a block control or by a filter.
What it loads
The script and styles load only on pages that actually contain the block. A page without one gets nothing.
Everything is bundled and served from your own site. There are no CDN requests, no Google Fonts, and no external requests of any kind at runtime.
The slider is img-comparison-slider, MIT licensed, about 12kB, with no runtime dependencies of its own. There is no jQuery on the front end.
Uninstalling
The plugin stores nothing. There are no options, no tables, and no post meta, so removing it leaves nothing behind. Blocks already in your content stop rendering, and their markup stays in the post until you delete it.