CKEditor Spacing
This module allows content editors to add margin and padding directly to text elements like paragraphs and headings within CKEditor. It provides a safe and easy way to adjust spacing without requiring custom CSS or allowing harmful inline styles. Spacing is saved as data attributes and converted to CSS on the page, with configurable units and formatting options.
CKEditor Spacing lets editors set margin and padding on individual blocks — paragraphs, headings, images — from a balloon in CKEditor 5, without writing CSS or enabling the style attribute.
Editors regularly need one paragraph to breathe a little more than the rest: extra room above a closing line, a heading pushed away from the text before it, some air around a pull quote. Until now that meant asking a developer for a CSS class, wrapping the text in a container it never needed, or opening the source view and typing inline styles.
That last option is the tempting one, and it is the reason this module exists. In Drupal, every rich-text field is governed by a text format, which decides what HTML editors are allowed to save. Letting the format keep the style attribute so that editors can type spacing by hand also lets anyone who can edit content paste arbitrary CSS — a real risk on any site with more than one author.
This module gives editors a small, safe control for exactly that job. Spacing is stored as harmless data attributes on the block itself, and a filter turns them into CSS when the page is rendered. The style attribute is never enabled, so a restricted text format stays restricted.
Features
- Margin and padding on any block. Paragraphs, headings, lists, blockquotes, images, tables — anything CKEditor treats as a block, including blocks added by other plugins.
- Free numeric values, not a fixed scale. Type the number you want and pick a unit: px, rem, em, %, vw or vh. Which units are offered is configurable per text format.
- Nothing is wrapped. The block keeps its identity; it simply gains an attribute, the same technique CKEditor's own alignment uses. No container is inserted into the markup.
- Live preview in the editor. Spacing is shown as you type, and Cancel restores whatever the block had when the balloon opened.
- An actions balloon, like links. Put the caret in a block that already has spacing and a small balloon appears with a summary of what is applied, plus edit and clear buttons.
- Visible without clicking. Corner markers appear around a spaced block on hover and while the caret is inside it — editor only, never saved.
- Safe by construction. Values are validated by the same grammar in JavaScript and in PHP. Negatives,
calc(), unknown units and injection attempts are rejected server-side, and the value is never concatenated into CSS unparsed. - Right-to-left aware. Logical CSS properties (
margin-block-start,padding-inline-start) are emitted, so spacing flips correctly in RTL languages. - Per-format control. Units and whether horizontal padding is offered at all are configured separately for each text format, so a restricted format can expose less than a full one.
- No dependencies, no build step. Plain ES6, no external libraries, no compiled assets.
When would I use this?
- A landing page where one section needs more separation than the theme's default rhythm.
- A heading that sits too close to the paragraph above it, in one place only.
- Giving an image or table room to breathe without editing the theme.
- Any one-off adjustment that does not deserve a new CSS class or a container.
If the same adjustment is needed over and over, that is a theme concern, not an editor one — this module is deliberately for the exceptions.
Post-Installation
There is no separate configuration page, and no new content type. Everything is set per text format at Administration › Configuration › Content authoring › Text formats and editors. Two steps are required, and both matter.
1. Add the toolbar button. Edit the text format and drag Spacing from the available buttons into the active toolbar.
2. Enable the filter. In the same form, tick “Apply spacing to block elements”. Then open Filter processing order and drag it after “Limit allowed HTML tags and correct faulty HTML”.
If you skip step 2, the editor will look like it works but published pages will show no spacing. The attributes are saved correctly; nothing converts them to CSS. This is the single most common setup mistake, so check the filter first if spacing is missing on the front end.
Once the button is in the toolbar, its settings appear below the toolbar configuration:
- Default unit — the unit selected when a field first gets a value. Defaults to px.
- Available units — which unit pills the balloon offers. Defaults to px, rem, em and %.
- Offer horizontal padding — off by default. Adds a field that pads left and right equally. It is opt-in because side padding on running text usually means a container is wanted instead. Turning it off later does not strip horizontal padding from existing content; the field simply disappears from the balloon.
What gets saved
The editor stores data attributes; the filter produces the CSS:
<!-- saved in the field --> <p data-spacing-mt="40px" data-spacing-pt="16px">…</p> <!-- rendered to the visitor --> <p style="margin-block-start: 40px; padding-block-start: 16px;">…</p>
Attribute reference: data-spacing-mt / data-spacing-mb are margin top and bottom, data-spacing-pt / data-spacing-pb padding top and bottom, and data-spacing-ps / data-spacing-pe the two horizontal padding sides. The prefix is deliberate: the attributes are declared on every element, so they must not collide with markup the site already uses.
Working with the balloon
Select or click into a block, press the Spacing button, and fill in the fields. Esc cancels and reverts the preview; Ctrl+Enter accepts. Blocks that already have spacing show a small balloon with a summary and shortcuts to edit or clear it, the same way links behave.
If something looks wrong
- Spacing shows in the editor but not on the page — the filter is off, or it runs before Limit allowed HTML tags. Step 2 above.
data-spacing-*is visible in the page source — same cause: nothing consumed the attributes, because the filter never ran.- A value disappears when you accept — it failed validation. Negative numbers, bare numbers with no unit,
calc()and units outside the configured list are all rejected. - The button is missing — it is added per format, so check the toolbar of the format that field actually uses.
Additional Requirements
Nothing beyond Drupal core. The module requires the core CKEditor 5 module and works on Drupal 10.3 and 11. There are no external libraries, no Composer dependencies and no build step — the JavaScript ships as written.
Recommended modules/libraries
- CKEditor Advanced Container — the right tool when a block needs a background, a border or grouping into columns, rather than just spacing. The two work together with no configuration: because this module emits an inline style, explicit spacing survives inside a container's columns.
- Show blocks (part of core CKEditor 5) — when enabled, this module's corner markers move outward so they do not overlap its outlines.
Similar projects
I am not aware of another module that does exactly this for CKEditor 5. The usual alternatives solve a nearby problem in a different way:
- The core CKEditor 5 “Style” plugin lets a site builder define named CSS classes that editors can apply. That is the better choice when the same spacing is used repeatedly and should stay tied to the design system. This module is for one-off values that do not deserve a class.
- Container or layout modules (including CKEditor Advanced Container) add spacing by wrapping content in a new element. That is heavier than a single paragraph usually needs, and it changes the markup structure. This module adds no elements at all.
- Allowing the
styleattribute in a text format achieves the same visual result, but opens the format to arbitrary CSS from anyone who can edit content. This module deliberately never enables it.
Supporting this Module
Maintained by Dom Host Seo. Issue reports and patches are welcome in the issue queue.
Community Documentation
The module's README covers the two setup steps, the attribute reference, the accepted value grammar, a troubleshooting table and the known limitations. It also documents the internals for anyone patching the module: the three CKEditor conversions, why strings must use Drupal.t(), and why the balloon's CSS needs three-class selectors to beat CKEditor's own reset.
The filter — the point where stored data becomes CSS — is covered by unit tests in tests/src/Unit/SpacingFilterTest.php, including injection attempts such as 10px; color: red and url(javascript:…). Run them with vendor/bin/phpunit -c core modules/contrib/ckeditor_spacing.
Known limitations
- Hand-written invalid values stay in storage. Drupal enables General HTML Support for the declared data attributes, so a value typed manually through the source view — for example
data-spacing-mt="abc"— remains in the saved HTML. It is inert: the filter rejects it and no style ever reaches the page. - Spacing is not responsive. A value applies at every viewport. Use relative units (rem, %, vw) if you need it to scale with the design.
- Margins still collapse. Adjacent vertical margins collapse exactly as CSS says they should, so a 40px bottom margin next to a 20px top margin yields 40px of gap, not 60px. Use padding when you need the space to be additive.