Skip to content

Instantly share code, notes, and snippets.

@stoffl6781
Last active September 23, 2025 07:07
Show Gist options
  • Select an option

  • Save stoffl6781/39d3f094bc3e069bba7a8c8114b3a9e3 to your computer and use it in GitHub Desktop.

Select an option

Save stoffl6781/39d3f094bc3e069bba7a8c8114b3a9e3 to your computer and use it in GitHub Desktop.
gutenberg-font-style.php

Gutenberg Inline Toolbar “THIN” Button (font-weight: 100)

Adds a small THIN button to the contextual inline toolbar (next to Bold/Italic) in the block editor.
When pressed, it wraps the current selection in a span that forces font-weight: 100. Press again on the same fully-wrapped selection to toggle off.


What it does

  • Injects a button labeled THIN into every contextual inline toolbar instance.
  • Wraps selection with <span data-fw="100" style='font-weight:100; font-variation-settings:"wght" 100;'>…</span>.
  • Unwraps if the selection already lives fully inside that span (toggle).
  • Keeps selection after wrapping for a smoother UX.
  • Adds a minimal editor-side CSS so the thin weight is visible immediately.

Keyboard Shortcut

Ctrl/Cmd + Alt + 1 — applies or toggles the “thin” wrapping for the current selection.


Installation

  1. Copy the PHP snippet into a small plugin, an mu-plugin, or the Code Snippets plugin (PHP mode, admin only).
  2. Open the block editor, select some text, and use the THIN button in the inline toolbar or the shortcut above.

Why not use a class?

Inline styling here ensures the editor preview reflects the thin weight even when the theme doesn’t map a class to font-variation-settings. You can adapt it to use a class if your theme guarantees font-weight: 100 support.


Notes

  • Works only inside contenteditable regions in the block editor.
  • Designed to be minimally invasive (adds/removes only the dedicated wrapper).
  • Tested with variable fonts: relies on font-variation-settings: "wght" 100 in addition to font-weight: 100.

<?php
/**
* Gutenberg Inline Toolbar: "THIN" Button (font-weight: 100)
*
* Adds a small "THIN" button to the contextual inline toolbar (the one with Bold/Italic)
* in the block editor. When clicked (or via the shortcut), it wraps the current selection
* inside <span data-fw="100" style='font-weight:100; font-variation-settings:"wght" 100;'>…</span>.
* Clicking again on a selection that is fully wrapped will unwrap it (toggle behavior).
*
* Keyboard shortcut: Ctrl/Cmd + Alt + 1
*
* Implementation notes
* - Injects a button into every contextual inline toolbar instance (once per toolbar).
* - Works only when the selection lies inside a contenteditable region in the editor.
* - Keeps the selection after wrapping by reselecting the inserted span's contents.
* - Also injects a minimal editor-side style so the weight is visible immediately.
*
* Usage
* - Place this into a small plugin, mu-plugin, or use Code Snippets (PHP, admin only).
*/
add_action('enqueue_block_editor_assets', function () {
// Inject JS after the editor scripts to ensure editor globals are available.
wp_add_inline_script('wp-block-editor', <<<'JS'
(function(){
const BTN_LABEL = 'THIN'; // Button label shown in the inline toolbar
// Attributes applied to the wrapping <span>.
const WRAP_ATTR = { 'data-fw': '100', style: 'font-weight:100; font-variation-settings:"wght" 100;' };
// ---- Helpers ---------------------------------------------------------------
// Safe accessor for the current DOM selection.
const sel = () => (window.getSelection && window.getSelection()) || null;
// Checks whether a Range is inside a contenteditable area of the editor.
function insideEditable(range){
if (!range || !range.commonAncestorContainer) return false;
const el = range.commonAncestorContainer.nodeType === 1
? range.commonAncestorContainer
: range.commonAncestorContainer.parentElement;
return !!(el && el.closest('[contenteditable="true"]'));
}
// Returns the closest <span data-fw="100"> wrapper for a node (or null).
function nearestThinSpan(node){
if (!node) return null;
const el = node.nodeType === 3 ? node.parentElement : node;
return el ? el.closest('span[data-fw="100"]') : null;
}
// Removes a wrapper span but keeps its children in place.
function unwrapSpan(span){
const parent = span.parentNode;
while (span.firstChild) parent.insertBefore(span.firstChild, span);
parent.removeChild(span);
}
// Wraps the current selection with a THIN span, or unwraps if already fully wrapped.
function wrapSelectionThin(){
const s = sel();
if (!s || s.rangeCount === 0) return;
const range = s.getRangeAt(0);
if (!insideEditable(range) || range.collapsed) return;
// Toggle behavior: if entire selection is within a single thin span -> unwrap it.
const startThin = nearestThinSpan(s.anchorNode);
const endThin = nearestThinSpan(s.focusNode);
if (startThin && endThin && startThin === endThin && startThin.contains(range.cloneContents())) {
unwrapSpan(startThin);
return;
}
// Otherwise: wrap selection.
const span = document.createElement('span');
Object.entries(WRAP_ATTR).forEach(([k,v])=> span.setAttribute(k,v));
const frag = range.extractContents();
span.appendChild(frag);
range.insertNode(span);
// Reselect the inserted content for a better UX.
const r = document.createRange();
r.selectNodeContents(span);
s.removeAllRanges();
s.addRange(r);
}
// ---- UI Injection ----------------------------------------------------------
// Inject a button into each inline toolbar (once per toolbar instance).
const seen = new WeakSet();
function injectInto(toolbar){
if (seen.has(toolbar)) return;
seen.add(toolbar);
// Find the main group (next to Bold/Italic). Skip if button already added.
const group = toolbar.querySelector('.components-toolbar-group, .components-toolbar');
if (!group || group.querySelector('[data-thin-btn]')) return;
const btn = document.createElement('button');
btn.type = 'button';
btn.className = 'components-button has-icon';
btn.setAttribute('aria-label', 'Font weight 100');
btn.dataset.thinBtn = '1';
btn.textContent = BTN_LABEL;
btn.style.fontSize = '11px';
btn.style.fontWeight = '600';
btn.style.letterSpacing = '0.6px';
btn.addEventListener('click', wrapSelectionThin);
group.appendChild(btn);
}
// Observe DOM mutations to catch contextual toolbars appearing.
const mo = new MutationObserver(mList => {
mList.forEach(m => {
m.addedNodes.forEach(n => {
if (!(n instanceof HTMLElement)) return;
// Contextual inline toolbar
if (n.matches && n.matches('.block-editor-block-contextual-toolbar')) injectInto(n);
n.querySelectorAll && n.querySelectorAll('.block-editor-block-contextual-toolbar').forEach(injectInto);
});
});
});
mo.observe(document.body, { childList: true, subtree: true });
// Keyboard shortcut: Ctrl/Cmd + Alt + 1
document.addEventListener('keydown', (e) => {
const mod = (e.ctrlKey || e.metaKey) && e.altKey && e.key === '1';
if (!mod) return;
const s = sel();
if (!s || s.rangeCount === 0) return;
if (!insideEditable(s.getRangeAt(0))) return;
e.preventDefault();
wrapSelectionThin();
});
console.log('[THIN] inline toolbar button active');
})();
JS, 'after');
// Editor preview styles so the thin weight is visible immediately in the editor.
wp_add_inline_style('wp-block-editor',
'.editor-styles-wrapper [data-fw="100"]{font-weight:100 !important; font-variation-settings:"wght" 100 !important;}'
);
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment