Page MenuHomePhabricator

The ability for users to enable/disable "birthday mode"
Closed, ResolvedPublic

Description

Background

As part of the Wikipedia 25 Easter egg celebrations, we want to give users the option of enabling a "birthday mode" on desktop and mobile Web.

On Vector 2022, we would like to add a "birthday mode" toggle switch to the appearance menu, and persist this option using the ClientPreferences functionality when the user enabled it.

Screenshot 2025-10-27 at 2.05.49 PM.png (687×417 px, 66 KB)

the "play mode" in the screenshot above

User story

As a reader, I'd like to be in control of whether or not I see the Wikipedia 25 birthday celebration experience, and I'd like to easily be able to turn it on or off.

Requirements

  • The ability for extensions to add an item to the Appearance menu in Vector 2022 via some kind of public interface.
  • The ability for extensions to use the client preferences functionality to persist a preference for logged-out users.

Implementation proposal

NOTE: The follow are just straw-man ideas, alternative suggestions are welcome.

This could potentially be a front-end mw.hook() that fires after the Appearance menu renders all the existing client preferences. Alternatively, or additionally, we could expose some of the private methods in clientPreferences.js, such as makeClientPreference(), in order to enable this functionality.

Event Timeline

Jdrewniak updated the task description. (Show Details)

Note, extending it does mean making it stable such that any gadget or user script or extension can then extend it. I am not sure if that's a good idea and it may be preferable to keep this as is.

You can currently extend it as I documented in https://www.mediawiki.org/wiki/Manual:Client_preferences for the Wikipedia 25th birthday, albeit there is currently no native support for a "switcher" Codex component - only radio buttons. Loading a switcher will also require adding additional CSS to all page views.

@Jdlrobson Thank you for documenting this functionality! Much appreciated!

I agree that exposing a public interface for this menu could expose it to gadgets, but I don’t think making it public necessarily means making it stable. Could we mark this method as experimental, or have it log a warning to that effect when it's used? I know that wouldn’t stop people from using it, but it would give us cover if we change or remove it.

My original suggestion to @ATitkov was to use a MutationObserver in place of a public API. Gadgets could technically do the same thing right now if they really wanted to, but they still wouldn’t be able to add a client preference, which is protected by the server-rendered HTML class, which only our server code can add. So in my opinion, the practical benefit for gadgets using this menu is pretty limited anyway.

Regarding the documentation, it explains how to add a client preference from inside Vector or Minerva, but I’m not sure adding this particular feature through the skins is the right approach.

Regarding the increase in CSS, out of curiosity I looked at the compiled output of the CSS for this component. From what I was able to paste in https://lesscss.org/less-preview/ from ToggleSwitch.vue It looks like it adds about 150 lines of CSS, which is certainly not nothing, but I don't think the performance impact would be visible because of this.

.cdx-toggle-switch {
  display: inline-flex;
  align-items: center;
  justify-content: flex-start;
  position: relative;
  z-index: 0;
  margin-bottom: 6px;
}
.cdx-toggle-switch--align-switch {
  display: flex;
  justify-content: space-between;
}
.cdx-toggle-switch:last-child {
  margin-bottom: 0;
}
.cdx-toggle-switch__label,
.cdx-toggle-switch__label.cdx-label {
  order: -1;
}
.cdx-toggle-switch__label:not( :empty ),
.cdx-toggle-switch__label.cdx-label:not( :empty ) {
  padding-right: 8px;
}
.cdx-toggle-switch .cdx-toggle-switch__label.cdx-label {
  padding-bottom: 0;
}
.cdx-toggle-switch .cdx-toggle-switch__label.cdx-label .cdx-label__label__text {
  font-weight: 400;
}
.cdx-toggle-switch__switch {
  /* stylelint-disable-next-line plugin/no-unsupported-browser-features */
  background-color: var(--background-color-base, #fff);
  display: inline-block;
  flex-shrink: 0;
  position: relative;
  box-sizing: border-box;
  min-width: 42px;
  min-height: 28px;
  width: calc(var(--font-size-medium, 1rem) * 3);
  height: calc(var(--font-size-medium, 1rem) * 2);
  border-width: 1px;
  border-style: solid;
  border-color: var(--border-color-interactive, #72777d);
  border-radius: 9999px;
  overflow: hidden;
  transition-property: background-color, color, border-color, box-shadow;
  transition-duration: 250ms;
}
.cdx-toggle-switch__switch::before {
  content: '';
  display: block;
  position: absolute;
  top: 1px;
  right: 1px;
  bottom: 1px;
  left: 1px;
  z-index: 1;
  border: 1px solid var(--border-color-transparent, transparent);
  border-radius: 9999px;
  transition-property: background-color, color, border-color, box-shadow;
  transition-duration: 250ms;
}
.cdx-toggle-switch__switch__grip {
  position: absolute;
  top: 50%;
  box-sizing: border-box;
  min-width: 18px;
  min-height: 18px;
  width: calc(var(--font-size-medium, 1rem) * 1.25);
  height: calc(var(--font-size-medium, 1rem) * 1.25);
  border: 1px solid var(--border-color-interactive, #72777d);
  border-radius: 50%;
  transform: translate(calc(var(--font-size-medium, 1rem) * 0.375), calc(-1 * 50%));
  transition-property: background-color, border-color, transform;
  /* stylelint-disable @stylistic/declaration-colon-newline-after,
				@stylistic/value-list-comma-newline-after */
  transition-duration: 250ms, 100ms, 100ms;
  /* stylelint-enable @stylistic/declaration-colon-newline-after,
				@stylistic/value-list-comma-newline-after */
}
.cdx-toggle-switch__input {
  opacity: 0;
  position: absolute;
  right: 0;
  z-index: 2;
  min-width: 42px;
  min-height: 32px;
  width: 3rem;
  height: 2rem;
  margin: 0;
  font-size: var(--font-size-medium, 1rem);
  /* stylelint-disable no-descending-specificity */
  /* stylelint-enable no-descending-specificity */
}
.cdx-toggle-switch__input:checked ~ .cdx-toggle-switch__switch .cdx-toggle-switch__switch__grip {
  background-color: var(--background-color-base, #fff);
  border-color: var(--border-color-inverted, #fff);
  transform: translate(calc(var(--font-size-medium, 1rem) * 1.25), calc(-1 * 50%));
}
.cdx-toggle-switch__input:enabled {
  /* stylelint-disable no-descending-specificity */
  /* stylelint-enable no-descending-specificity */
}
.cdx-toggle-switch__input:enabled:hover,
.cdx-toggle-switch__input:enabled ~ .cdx-label .cdx-label__label:hover,
.cdx-toggle-switch__input:enabled ~ .cdx-toggle-switch__label:not( .cdx-label ):hover {
  cursor: pointer;
}
.cdx-toggle-switch__input:enabled ~ .cdx-toggle-switch__switch .cdx-toggle-switch__switch__grip {
  background-color: var(--background-color-base, #fff);
}
.cdx-toggle-switch__input:enabled:hover ~ .cdx-toggle-switch__switch {
  background-color: var(--background-color-interactive-subtle--hover, #eaecf0);
  border-color: var(--border-color-interactive--hover, #27292d);
}
.cdx-toggle-switch__input:enabled:hover ~ .cdx-toggle-switch__switch .cdx-toggle-switch__switch__grip {
  background-color: var(--background-color-base, #fff);
  border-color: var(--border-color-interactive--hover, #27292d);
}
.cdx-toggle-switch__input:enabled:active ~ .cdx-toggle-switch__switch {
  background-color: var(--background-color-interactive-subtle--active, #dadde3);
  border-color: var(--border-color-interactive--active, #202122);
}
.cdx-toggle-switch__input:enabled:active ~ .cdx-toggle-switch__switch .cdx-toggle-switch__switch__grip {
  background-color: var(--background-color-base, #fff);
  border-color: var(--border-color-interactive--active, #202122);
}
.cdx-toggle-switch__input:enabled:focus:not( :active ) ~ .cdx-toggle-switch__switch {
  box-shadow: inset 0 0 0 1px var(--box-shadow-color-progressive--focus, #36c);
  outline: 1px solid transparent;
}
.cdx-toggle-switch__input:enabled:checked ~ .cdx-toggle-switch__switch {
  background-color: var(--background-color-progressive, #36c);
  border-color: var(--border-color-transparent, transparent);
}
.cdx-toggle-switch__input:enabled:checked ~ .cdx-toggle-switch__switch .cdx-toggle-switch__switch__grip {
  background-color: var(--background-color-base-fixed, #fff);
  border-color: var(--background-color-transparent, transparent);
}
.cdx-toggle-switch__input:enabled:checked:hover ~ .cdx-toggle-switch__switch {
  background-color: var(--background-color-progressive--hover, #3056a9);
  border-color: var(--border-color-transparent, transparent);
}
.cdx-toggle-switch__input:enabled:checked:focus:not( :active ) ~ .cdx-toggle-switch__switch {
  border-color: var(--border-color-transparent, transparent);
}
.cdx-toggle-switch__input:enabled:checked:focus:not( :active ) ~ .cdx-toggle-switch__switch::before,
.cdx-toggle-switch__input:enabled:checked:focus:not( :active ) ~ .cdx-toggle-switch__switch .cdx-toggle-switch__switch__grip {
  border-color: var(--border-color-inverted, #fff);
}
.cdx-toggle-switch__input:enabled:checked:active ~ .cdx-toggle-switch__switch {
  background-color: var(--background-color-progressive--active, #233566);
  border-color: var(--border-color-transparent, transparent);
  box-shadow: inset 0 0 0 1px var(--box-shadow-color-progressive--active, #233566);
}
.cdx-toggle-switch__input:enabled:checked:active ~ .cdx-toggle-switch__switch::before {
  border-color: var(--border-color-transparent, transparent);
}
.cdx-toggle-switch__input:enabled:checked:active ~ .cdx-toggle-switch__switch .cdx-toggle-switch__switch__grip {
  background-color: var(--background-color-base-fixed, #fff);
  border-color: var(--border-color-transparent, transparent);
}
.cdx-toggle-switch__input:disabled {
  cursor: default;
}
.cdx-toggle-switch__input:disabled ~ .cdx-toggle-switch__switch {
  background-color: var(--background-color-disabled-subtle, #eaecf0);
  border-color: var(--border-color-disabled, #c8ccd1);
}
.cdx-toggle-switch__input:disabled ~ .cdx-toggle-switch__switch .cdx-toggle-switch__switch__grip {
  border-color: var(--border-color-disabled, #c8ccd1);
}
.cdx-toggle-switch__input:disabled:checked ~ .cdx-toggle-switch__switch {
  background-color: var(--background-color-disabled, #dadde3);
  border-color: var(--border-color-transparent, transparent);
}
.cdx-toggle-switch__input:disabled:checked ~ .cdx-toggle-switch__switch .cdx-toggle-switch__switch__grip {
  background-color: var(--color-disabled-emphasized, #a2a9b1);
  border-color: var(--border-color-transparent, transparent);
}
Jdrewniak renamed this task from Enable extensions to add new items to Appearance menu to The ability for users to enable/disable "birthday mode".Nov 4 2025, 5:38 PM
HFan-WMF triaged this task as High priority.Nov 5 2025, 4:42 PM
HFan-WMF moved this task from Incoming to Backlog on the Reader Experience Team board.

@bwang pointed out this is already technically supported (albeit for mobile). We already have a binary toggle for "width" on desktop but that uses radio buttons so it is worth considering whether using a toggle switch on desktop confuses the UI. I would recommend thinking about this from a consistency point of view as part of this task (if width is a radio and birthday mode is a toggle switch it should be clear why these are different in the UI!).

Hi folks thanks for raising this. While we can certainly make a radio button work, I do think that the binary radio toggle for width is a pretty different use case, where you are choosing between two named styles (wide and standard). For the birthday related mode, this would require setting a theme of 'Name tbd birthday mode' and 'Classic'.

Change #1205217 had a related patch set uploaded (by ATitkov; author: artemkloko):

[mediawiki/skins/Vector@master] Add client preference for WP25EasterEggs extension

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

Change #1205218 had a related patch set uploaded (by ATitkov; author: artemkloko):

[mediawiki/extensions/MobileFrontend@master] Add mobile client preference for WP25EasterEggs extension

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

Change #1205219 had a related patch set uploaded (by ATitkov; author: ATitkov):

[mediawiki/extensions/WP25EasterEggs@master] Add Community Configuration and i18n messages

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

Change #1206469 had a related patch set uploaded (by Aude; author: Aude):

[integration/config@master] Zuul: [mediawiki/extensions/WP25EasterEggs] Add CommunityConfiguration dep

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

Change #1205217 merged by jenkins-bot:

[mediawiki/skins/Vector@master] Add client preference for WP25EasterEggs extension

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

Change #1205218 merged by jenkins-bot:

[mediawiki/extensions/MobileFrontend@master] Add mobile client preference for WP25EasterEggs extension

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

Change #1205219 merged by jenkins-bot:

[mediawiki/extensions/WP25EasterEggs@master] Add Community Configuration and i18n messages

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

Change #1206469 abandoned by Hashar:

[integration/config@master] Zuul: [mediawiki/extensions/WP25EasterEggs] Add CommunityConfiguration dep

Reason:

This was done via other changes :)

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