You Can Make a Visual Novel in MediaWiki
You know what I love the most? Dead easy things with infinite ways to be utilized and expanded upon! And the prime example of that on my favorite software? That's custom toggles for ya!
Ever since MediaWiki 1.18, the default installation has always bundled with it the makeCollapsible module, which is, of course, used to make collapsibles. At a glance, you may think that's all there is to it. But, I wouldn't blame you at all! For most, if not all, cases where it is going to be used, it's going to be exactly what it advertised itself to be:
<div class="mw-collapsible mw-collapsed">
I am hidden, unless you opened me!
</div>It lets one create a collapsible! So, how do we go from there to a whole visual novel? I mean, all it can do is collapse and expand itself, right?
Well, those simple collapsibles obviously aren't going to cut it; that's where the custom toggles I've mentioned at the start come into place:
<span class="mw-customtoggle-toggle">Click me to see the hidden text</span>
<div class="mw-collapsible mw-collapsed" id="mw-customcollapsible-toggle">
Le hidden text...
</div>
Now, from the example above, through the usage of mw-customtoggle, we can define a toggler in its own place to collapse or expand something somewhere else. Basically, you get a flip-flop!
This is where the infinite possibilities come from...
<span class="
mw-collapsible
mw-customtoggle-toggler1
mw-customtoggle-toggler2
mw-customtoggle-hiddentext
"
id="mw-customcollapsible-toggler1">Click me to see the hidden text</span>
<span class="
mw-collapsible
mw-collapsed
mw-customtoggle-toggler2
mw-customtoggle-toggler1
mw-customtoggle-hiddentext
"
id="mw-customcollapsible-toggler2">Click me to close the hidden text</span>
<div class="mw-collapsible mw-collapsed" id="mw-customcollapsible-hiddentext">
'''Le hidden text...'''
</div>
Okay, okay! I know those elements look cursed, but really, it's for readability, I swear! Anyhow, by assigning IDs and letting the toggles toggle themselves, we can make a pair of mutually exclusive states. One toggle effectively becomes the 'set' state while the other becomes the 'reset' state, with both controlling the visibility of the same piece of content.
In other words, we've essentially made ourselves a cross-coupled SR latch! I really want to nail the flip-flop parallel home here, haha. There's not actually any real feedback between the 'gates' (in this case, the buttons), of course, as we've merely mocked them. However, conceptually and more importantly, visually, it's exactly identical.
Making a Visual Novel
With the basics out of the way, we can finally get to the cool part now:
That's the visual novel demo I made in 2023 with the custom collapsible classes, pretty neat, right? It took a long while to get the toggles right; I even had a diagram to visualize everything, actually! Though it's a bit of a shame that's lost now, ouch.
Anyway, in order to get something this complicated working, you need to be extremely imperative with your toggles. As they're logic-based, you need to map out which toggle which. For example, in the visual novel, I had to map out which dialogue can grant the user the ability to move, which to add content to the journal mechanic, and so on.

That's how a dialogue looks; before you blame me, I have no idea why young me thought cramming everything in a single line was a great idea, or not using templates for that matter.
Eh, whatever. As you can see, there's a lot of that cross-coupled SR logic I explained before. We hook one toggler to multiple elements at once, disable any that are no longer needed (such as old dialogues), and toggle the new ones to take its place. The whole thing is like a state machine!
For example, the arrow itself in the dialogue box is a toggler for the next available line. It collapses itself, then expands the next line, and while doing that, it toggles KirBigHello off and KirBigQuestion on so the Landlord's sprite changes with the text. This goes on for each line, until it reaches the endpoint where it sets anything that needs to, say, be unlocked for the user, such as journal logs or new abilities. This is how I planned for key items as well; too bad I never got around to implementing it. You could make items unlock 'buttons' that were previously hidden because it wasn't activated.
Hence, this pattern is the bread and butter of everything in the visual novel, and I'm quite proud of myself for using it to this extent. It does, of course, make everything really hard to maintain, though. One has to keep track of all the toggles and their dependencies, meaning one small change can easily domino everything else.
For the buttons and positions of characters, I had stupidly manually drawn everything through position: absolute and position: relative. A better way that I'd approach this now is through a mix of grid and flexbox, then the positions as needed for the environment. I was a young and naïve fellow in the world of web development, haha. I also used tabbers for the journal too (I suppose you could say this implementation isn't truly default MediaWiki, but whatever, bite me), which was awesome as collapsibles handle the entries pretty well, since they do not create ghost spaces in the layout if they haven't been expanded yet.
Using Lua Instead
If you actually want to not bear the pain of doing everything manually and use Scribunto, you could get a lot done. I personally haven't revisited the Lua approach for the visual novel yet, and probably won't for a while since life has been fairly tough on me lately.
Though, I still have utilized this in other situations that aren't for the 'gaming' gimmick, more practical ones, to be precise.
This is a conversation module called Converse that I wrote for use on Fandom wikis, as JavaScript isn't loaded on mobile, which is a significant blow for parity.
This is what I did in the module, and all that you probably need to get everything started in Lua:
local toggles = 'mw-customtoggle-' .. groupID
.. ' mw-customtoggle-' .. resultID
if branchDivID then
toggles = toggles .. ' mw-customtoggle-' .. branchDivID
endEverything else is just to get it to do branching and whatnot for the template.
Lua doesn't replace the collapsible pattern. In the code, _renderDialogue gets the appropriate text bubble version (there are three for NPC, player, and actions), and _renderSequence walks the stepN-* arguments until it hits a row of options. Each option will then become one of those multi-target toggles from earlier! Hide the menu, show that reply, and if you set continue to a label, uncollapse the matching stepN-branch block. There's recursion handling the forks, so I don't have to be wiring every path by hand this time.
math.randomseed(os.clock() * 777777777)
local runID = 'conv-' .. math.random(100000, 999999)The only other thing worth mentioning is the random conv-###### prefix. As collapsible IDs are global to the page, if you were to use this same template on the same page, they will collide. So you'll have to use a technique with the math library to get a unique PRNG for each invocation.
With all of that, instead of the absolute disaster such as the visual novel earlier, one can simply write:
{{Converse|
...
| step30-type = option
| step30-text = "Eject the cargo."
| step30-continue = no
| step40-type = option
| step40-text = "Find another way."
| step40-continue = bypass
...
}}
It's much easier on the eyes and a thousand times more maintainable than doing it manually.
Other Usages
I know that the article's title said it was supposed to be about a visual novel, but hey, it's just bait, buddy.
The thing I want to show here is how versatile and flexible custom toggles are, and just to drive the point home, here's how you can use them to create a loading screen!
To put it simply, as the ResourceLoader requires some time to load JavaScript modules, it can take a few seconds before the jquery.makeCollapsible runs and the page is fully interactive. We can take advantage of this by defining a collapsed element at the very start of the source, which is the loading screen, and a custom toggle that 'collapses' it. You could technically put them anywhere, but I personally place them at the start for semantics and also to account for the minuscule difference in when it gets to appear.
<div class="mw-collapsible mw-collapsed" id="mw-customcollapsible-69" style="position: fixed; left: 0; top: 0; width: 100%; height: 100%; background: #FBF3E8; z-index: 1337">
<div style="position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); max-height: 80%;">
https://static.wikia.nocookie.net/tower-defense-sim/images/7/7e/Mumeiloading.gif
</div></div><span class="mw-customtoggle-69"></span>Core's CSS, for good reason, does not hide a collapsed element, as it does not know that it can actually be toggled, therefore letting it paint. The dummy custom toggle is only there so MediaWiki does not slap its fallback '[Expand]' link and prevent it from disappearing. By the time it sees the toggle and collapses our loading screen overlay, pretty much everything else has been loaded. Thus, it is an accurate loading screen.
Pretty cool, eh?