Page MenuHomePhabricator

Skin: add flexibility to skin-invert-image, improve readability of inverted diagrams or formulas in dark/night mode
Open, LowPublicFeature

Description

Feature summary
Give article authors the flexibility to optionally saturate images of the class skin-invert-image. Often, svg diagrams or formula in light mode use well saturated colors to hint the reader to specific aspects of the image. However, if the image is inverted for dark mode with the CSS rule filter: invert(1) hue-rotate(180deg) the colors are pale / dull and are not as useful as in light mode to differentiate parts of the image.

To demonstrate the issue, please refer to the following image, it shows example snippets for light mode, dark mode and a third part with the proposed feature, here adding saturate(60) at the end of the CSS filter chain, from left to right.

dark-mode-skin-invert-image-without-and-with-saturate-filter.png (448×176 px, 29 KB)

  • left part: light mode, no filter
  • middle part: dark mode, status quo
  • rightmost part: filter: invert(1) hue-rotate(180deg) saturate( var( --inv-saturate, 100% ) ) with variable inv-saturate set to a value of 60

Since authors may need to choose a sensible saturate value themselves, the proposed patch defines classes that can be used in conjunction with skin-invert-image to set this value in wikitext, for the example shown: [[File:....|class=skin-invert-image inv-saturate-60]] would be the syntax to configure the filter as to produce the result in the rightmost part.


A legend text, if present, explaining the color code used, needs to be stylable in the same manner, hence <span class="skin-invert inv-saturate-60">..</span> should work likewise for colored text.

The proposed patch ensures this by using inv-saturate-* class selector without coupling it to the presence of a skin-invert-* class. This means it could also be defined independently using a wrapping element somewhere in the DOM, although this is discouraged, see Example4 below.

Use case(s)
appearance of all wikipedia articles with diagrams or formulas, e. g. with a chemistry topic, when viewed with dark mode accessibility feature

Benefits
better readability of inverted diagrams or formulas in dark/night mode


Examplary usage in wikitext
Example1
[[File:Carboxamides.svg|130px|class=skin-invert-image inv-saturate-60]]
[[File:Tert-Sulfonicacid Amide Structure V.1.svg|120px|class=skin-invert-image inv-saturate-60]]
Example2
{| class="wikitable skin-invert-image inv-saturate-60"
|-
| [[File:Carboxamides.svg|130px]]
| [[File:Tert-Sulfonicacid Amide Structure V.1.svg|120px]]
|}
Example3
<div class="skin-invert-image inv-saturate-60">
[[File:Carboxamides.svg|130px]]
[[File:Tert-Sulfonicacid Amide Structure V.1.svg|120px]]
</div>
Example4 (discouraged)

This is discouraged for the reason mentioned above. It works as per CSS style definition, but it is harder to read, esp. if the div is filled with more text and the distance between the class assignments increases.

<div class="inv-saturate-60">
lorem ipsum ..
[[File:Carboxamides.svg|130px|class=skin-invert-image]]
[[File:Tert-Sulfonicacid Amide Structure V.1.svg|120px|class=skin-invert-image]]
</div>

Event Timeline

Change #1156449 had a related patch set uploaded (by Gerrit Patch Uploader; author: My Name):

[mediawiki/skins/Vector@master] Skin: improve readability of skin-inverted diagrams or formulas in dark/night mode

https://gerrit.wikimedia.org/r/1156449

The patch original depended on T288498 because it used the each() function of lesscss not yet ported to less.php.
It has been rewritten to cope without each().

@Pppery: rechecked https://gerrit.wikimedia.org/r/1156449

saturate function missed an escape, because it is both, a less function and a CSS filter function

The exception was thrown, because the LESS parser tried to interpret it as a less function call, refer to https://lesscss.org/functions/#color-operations-saturate ; to pass it through the parser as a CSS literal, LESS offers two possibilities:

Thanks for working on the ticket.

Please do not ping me on this ticket. I have my own processes for finding patches needing a recheck and don't find the ping useful.

Jdrewniak subscribed.

hi @Hechtsuppe thanks for the idea and the patch, as well as the effort of ensuring the additional CSS output is pretty minimal (screenshot below). The Web team would like to evaluate this from more of a long-term maintenance perspective before merging the patch, since these classes ultimately represent a public API in the skin.

Screenshot 2025-06-23 at 11.38.26 AM.png (2,748×1,300 px, 568 KB)

The acceptance of the dark-mode usability enhancements, specifically rendering of inverted images, does highly benefit from these changes.

Imho, as long as this is unavailable it is a reason for lots of reverts downstream, then claiming that 'dark-mode' is merely a POV (point-of-view) feature, and, at will, pointless.

However, a lot of studies show, that reading experience in the dark / night color model is not only beneficial for sight-impaired readers, but more general for all readers, depending on time-of-day usage.

I do not oppose evaluation, take your time, but please do not schedule this as 'nice to have' or 'low'.

Its a point missing that should have been implemented together with the Vector 2022 (Desktop improvements) by the web team – given the complexity of color models and accessible inversion renderings, I do not claim however, that this would have been an easy-to-spot implementation detail.

Best regards.

The CSS variable name --skin-invert-saturate has been chosen as to be descriptive and
not hinder future additions to the CSS / less code that might need the term saturate in other context.

The abbreviation in inv-saturate-*, instead of spelled out variant skin-invert-saturate-*, has been specifically chosen, because it is expected that this CSS class name may be repeated a lot of times in an individual article, depending on media usage.

Because it is expected to generally be used in combination with CSS class skin-invert-image, there is
no loss on descriptivity (due to that abbreviation). More so, it would not be elegant to read assignments
|class=skin-invert-image skin-invert-saturate-60
or alike in wikitext, when the public API is employed.

The reason to not also abbreviate the CSS variable name itself stems from the wish for the code to be self-documenting and additionally, because the stylesheet itself is loaded once per pageview and thus repetition is less of an issue as when authoring wikitext.


If you find the class names incomprehensive or badly named, then please, go ahead and adjust them. I'm strong about the feature, not specifically on the implementation detail.

Implementation variant

The following is a sketch to eventually discuss. It is an alternative implementation to the one already proposed. Some notable differences are

  • more generic solution at the expense of code readability
  • employs 'contains' variant (*=) of the CSS3 Substring Matching Attribute Selectors, instead of .class selector
    • (may need benchmarking to check if performance is in the same class)
  • compound class name – in wikitext one class name only, instead of two, toggles the skin-invert features, example:
    • |class=skin-invert-image-saturate-60 vs |class=skin-invert-image inv-saturate-60

lesscss code (with omissions):

html.skin-theme-clientpref-os {
  [class*="skin-invert-image"] img,
  [class*="skin-invert"] {
    color-scheme: light;
    filter: invert( 1 ) hue-rotate( 180deg ) e( 'saturate(var(--saturate, 100%))' );
  }
}

.saturate-classes( os );

@values: 0, 1, 10, 20, 30, 60, 90, 120;
.loop( @pref, @i ) when ( @i > 0 ) {
	@v: extract( @values, @i );
	html.skin-theme-clientpref-@{pref} [class*="-saturate-@{v}"] {
		--saturate: @v;
	}
	.loop( @pref, @i - 1 );
}
.saturate-classes( @pref ) {
	.loop( @pref, length( @values ) );
}

CSS result (with omissions):

html.skin-theme-clientpref-os [class*="skin-invert-image"] img,
html.skin-theme-clientpref-os [class*="skin-invert"] {
  color-scheme: light;
  filter: invert(1) hue-rotate(180deg) saturate(var(--saturate, 100%));
}
html.skin-theme-clientpref-os [class*="-saturate-120"] {
  --saturate: 120;
}
/* output omitted .. */
html.skin-theme-clientpref-os [class*="-saturate-60"] {
  --saturate: 60;
}
/* output omitted .. */

Without exploding the ruleset for every possible combination, this were to match a range of CSS class names to be used in wikitext as follows

  • either skin-invert or skin-invert-image as prefix and
  • one of -saturate-0, -saturate-1, etc. as suffix

This implementation variant does not strictly bind the term saturate to context, in contrast to the earlier implementation. In consequence [class*="-saturate-@{v}"] and the CSS variable name --saturate may later on be reusable in combination with other class name sets, targeting different features. In its simplest form e. g.

[class*="skin-saturate-"] {
  filter: saturate(var(--saturate, 100%));
}

Caveats:
This extensibility may be desirable, or not. If it is favorable, then as a precondition, it should be checked whether multiple assignments to --saturate may occur from a /use case perspective/. For example, |class=skin-invert-image-saturate-60 skin-saturate-10 would assign to the variable twice, but all backing rules will read the same one value. Thus, the design decision assumes that there are no use cases for this in practice. (Otherwise the variable name should be safe-guarded with additional chars and specifically pair with a feature prefix, as in the original implementation.)


For the following reasons the original implementation is preferable, imho:

  • more specific naming, less potential naming conflicts
  • conventional CSS class names (may perform better than substring matching (neglectable?) )
  • easier code readability (spelled out class names vs prefix / suffix matching strategy)
  • more minimal change, less experimental
  • may still borrow from the concepts above by refactoring the code to be more generic at a later time

Since the monolithic (compound) class name may be favorable to offer in the public API, for example it disallows fragmentation shown in the discouraged Example4 above, find the full patch below regardless. There may still be a better way to implement it, but it adds to the pool of ideas to draw from.


Full patch for this implementation variant:

diff
diff --git a/resources/skins.vector.styles/CSSCustomProperties.less b/resources/skins.vector.styles/CSSCustomProperties.less
index 15facde0..fa375cf4 100644
--- a/resources/skins.vector.styles/CSSCustomProperties.less
+++ b/resources/skins.vector.styles/CSSCustomProperties.less
@@ -44,13 +44,16 @@
 	 * - T365102 invert class specifically for image related element
 	 * - T365764 special treatment of inverted OOUI icons
 	 */
-	html.skin-theme-clientpref-night .skin-invert-image img,
-	html.skin-theme-clientpref-night .skin-invert,
-	html.skin-theme-clientpref-night @{OOUIIconSelectors},
-	html.skin-theme-clientpref-night @{OOUIIndicatorSelectors} {
-		color-scheme: light;
-		filter: invert( 1 ) hue-rotate( 180deg );
+	html.skin-theme-clientpref-night {
+		[class*="skin-invert-image"] img,
+		[class*="skin-invert"],
+		@{OOUIIconSelectors},
+		@{OOUIIndicatorSelectors} {
+			color-scheme: light;
+			filter: invert( 1 ) hue-rotate( 180deg ) e( 'saturate(var(--saturate, 100%))' );
+		}
 	}
+	.saturate-classes( night );
 
 	/**
 	 * Forced light mode.
@@ -139,11 +142,31 @@
 	}
 
 	// T365102 invert class specifically for image related elements
-	html.skin-theme-clientpref-os .skin-invert-image img,
-	html.skin-theme-clientpref-os .skin-invert,
-	html.skin-theme-clientpref-os @{OOUIIconSelectors},
-	html.skin-theme-clientpref-os @{OOUIIndicatorSelectors} {
-		color-scheme: light;
-		filter: invert( 1 ) hue-rotate( 180deg );
+	html.skin-theme-clientpref-os {
+		[class*="skin-invert-image"] img,
+		[class*="skin-invert"],
+		@{OOUIIconSelectors},
+		@{OOUIIndicatorSelectors} {
+			color-scheme: light;
+			filter: invert( 1 ) hue-rotate( 180deg ) e( 'saturate(var(--saturate, 100%))' );
+		}
 	}
+	.saturate-classes( os );
+}
+
+/**
+ * Intensity defining rule set for a saturation filter optionally
+ * applied at the end of a skin-invert-* filter chain
+ *
+ */
+@values: 0, 1, 10, 20, 30, 60, 90, 120;
+.loop( @pref, @i ) when ( @i > 0 ) {
+	@v: extract( @values, @i );
+	html.skin-theme-clientpref-@{pref} [class*="-saturate-@{v}"] {
+		--saturate: @v;
+	}
+	.loop( @pref, @i - 1 );
+}
+.saturate-classes( @pref ) {
+	.loop( @pref, length( @values ) );
 }

Change #1156449 abandoned by Jdlrobson:

[mediawiki/skins/Vector@master] Skin: improve readability of skin-inverted diagrams or formulas in dark/night mode

Reason:

Needs further discussion.

https://gerrit.wikimedia.org/r/1156449