Image Carousel


[[iftags +template]]

What this is

A bunch of miscellaneous CSS improvements. "Improvements" is subjective. I do not care.

If you're not me, which you're not (I think), then you definitely shouldn't use this component because it may break, disappear, or change in a way that you don't like at any time; and I promise you that if I want to add something to this I will without asking your permission or checking whether or not it breaks your article. There's a reason this isn't on the main wiki. But other than that go wild.


[[include :topia:cqb:css]]

Using this on a page tagged 'template'? You'll need to manually stop all these usage instructions from appearing:

[[div style="display: none;"]]
[[include :topia:cqb:css]]


Reasonably-sized footnotes

Stops footnotes from being a million miles wide, so that you can actually read them.

.hovertip { max-width: 400px; }

I'd add a demonstration, but I've included this component to a bunch of other template pages and it messes them up. You'll just have to trust me on this one.

Monospace edit/code

Makes the edit textbox monospace, and also changes all monospace text to Fira Code, the obviously superior monospace font.

    font-family: 'Fira Code';
    src: url('') format('embedded-opentype'),
    url('') format('woff2'),
    url('') format('woff'),
    url('') format('truetype');
    font-weight: normal;
    font-style: normal;
:root { --mono-font: "Fira Code", Cousine, monospace; }
#edit-page-textarea, .code pre, .code p, .code, tt, .page-source { font-family: var(--mono-font); }
.code pre * { white-space: pre; }
.code *, .pre * { font-feature-settings: unset; }

Fuck off bigfaces

Stops big pictures from appearing when you hover over someone's avatar image, because they're stupid and I hate them.

.avatar-hover { display: none !important; }

Breaky breaky

Break links if I want them to

.nobreak { word-break: break-all; }

Code colours

Add my terminal's code colours as variables

:root {
  --c-bg: #393939;
  --c-syntax: #e0e0e0;
  --c-comment: #999999;
  --c-error: #f2777a;
  --c-value: #f99157;
  --c-symbol: #ffcc66;
  --c-string: #99cc99;
  --c-operator: #66cccc;
  --c-builtin: #70a7df;
  --c-keyword: #cc99cc;
.terminal {
  color: var(--c-syntax);
  background: var(--c-bg);
  border: 0.5rem solid var(--c-comment);
  border-radius: 2rem;

Debug mode

Draw lines around anything inside .debug-mode.

.debug-mode, .debug-mode *, .debug-mode *::before, .debug-mode *::after {
  outline: 1px solid var(--debug-colour, red);
.debug-info {
  position: absolute;
  left: 50%;
  transform: translateX(-50%);
  font-family: 'Fira Code', monospace;
  font-size: 1rem;
  white-space: nowrap;
.debug-info.over { top: -2.5rem; }
.debug-info.under { bottom: -2.5rem; }
.debug-info p { margin: 0; }



Wherever you want your Image Carousel, slap in this code instead of your standard image block.

Make sure that any images you want are already uploaded to your page. If you chose to ignore that sage advice, then your images won't work until you Hard Refresh the page (Ctrl+Shift+R).

[[include :topia:cqb:carousel
| images=photograph.png,old-map.png,jumpscare.gif
| caption=A selection of images.
| interval=5
| wiki=scp-wiki
| page=SCP-173
| width=300px
| height=240px
| position=right
| no-caption=false
| background=white
| options=yes

What each option is

Anything in italics is optional. Everything else you gotta have.

If you omit an optional option, then it will have its default value. If you omit a non-optional option, then don't expect the carousel to work properly.

images: A list of images, separated by commas. Be sure that each one of these images is uploaded to the page.
These images will appear in the carousel in the order that you list them.
The caption that goes underneath the carousel.
If you have no caption, make sure you set no-caption to true.
Default value: "{$caption}"
If you set this to a non-zero number, then the carousel will automatically move onto the next image after this number of seconds.
If the user has clicked an arrow to manually change the image, or if they are currently hovering their mouse over the carousel, then the image will not rotate.
Default value: "0"
wiki: The name of the wiki that the page that you want the carousel to be on, is on. For example, scp-wiki, scp-sandbox-3, or topia.
page: The name of the page that you want the carousel to be on. Include the category, if any.
The width of the widest image in the carousel.
Default value: "300px"
The height of the tallest image in the carousel.
Default value depends on browser
When you want this to be on the page. "left", "right" or "center".
Default value: "right"
Set this to "true" if you don't want a caption. Otherwise, leave it blank , or set it to "false", or get rid of it completely.
Default value: "false"
The background colour behind the images.
Default value: "transparent"
Do you want the detailed options (play/pause button and the row of little circles) to display? If not, set to anything but "yes".
Default value: "yes"

I want the carousel to spread across the whole page so I can be like kaktus but cooler!
Set width to "100%" and position to "center".

I set width/height to the size of my biggest image but it's way too big!
Pick a smaller number, or make your images smaller.


HTML structure of the carousel

<html ng-app="carousel" ng-controller="CarouselController">
    <script src=""></script>
    <script src=""></script>
    <link href="" rel="stylesheet">
    <link href="" rel="stylesheet">
    <link href="" rel="stylesheet">
    <div class="wrapper" id="background">
        <div class="carousel">
            <div class="horsie" ng-repeat="image in images track by $index"
                 ng-class="[index > $index ? 'past' : null,
                            index === $index ? 'present' : null,
                            index < $index ? 'future' : null]">
                <img ng-src="{{image}}">
        <div class="arrow decrementor"
             ng-class="index === 0 ? 'inactive' : 'active'"
            <div class="image"></div>
        <div class="arrow incrementor"
             ng-class="index === images.length-1 ? 'inactive' : 'active'"
            <div class="image"></div>
        <div class="bubble-holder" ng-class="[options === 'yes' ? null : 'invisible']">
            <div class="bubble" ng-repeat="image in images track by $index"
                 ng-class="[index === $index ? 'present' : null]"
        <div class="control play" ng-click="control('play')"
             ng-class="[state === 'play' ? 'active' : null,
                        options === 'yes' ? null : 'invisible']"></div>
        <div class="control pause" ng-click="control('pause')"
             ng-class="[state === 'pause' ? 'active' : null,
                        options === 'yes' ? null : 'invisible']"></div>

JS to operate the carousel

function getQueryVariable(variable) {
    var query = document.location.href.match(/\?.*$/g)[0].substring(1);
    var vars = query.split("&");
    for(var i = 0; i < vars.length; i++) { // >
        var pair = vars[i].split("=");
        if(pair[0] === variable) return pair[1]; 
    return false;
    var carousel = angular
    CarouselController.$inject = ['$scope','$timeout'];
    function CarouselController($scope,$timeout){
        $scope.images = getQueryVariable("images").split(",");
        var wiki = getQueryVariable("wiki") || "scp-wiki";
        var page = getQueryVariable("page");
        for(var i = 0; i < $scope.images.length; i++) { // >
            $scope.images[i] = `http://${wiki}${page}/${$scope.images[i]}`;
        // $scope.images is an array of image URLs
        $scope.index = 0;
        $scope.increment = function(amount) {
            if(amount > 0 && $scope.index < $scope.images.length-1) { 
                $scope.index += amount;
            if(amount < 0 && $scope.index > 0) { 
                $scope.index += amount;
            $scope.state = "pause";
        var interval = getQueryVariable("interval") || 0;
        if(interval === "{$interval}") interval = 0;
        $scope.state = "play";
        if(interval === 0) $scope.state = "pause";
        function oscillate() {
            $timeout(function() {
                if(!mouseover && $scope.state === "play") {
                    if($scope.index < $scope.images.length-1) {
                    } else {
                        $scope.index = 0;
                if($scope.state === "play") {
            }, interval*1000, true);
        var mouseover = false;
        document.documentElement.onmouseover = function () {
            mouseover = true;
        document.documentElement.onmouseout = function () {
            mouseover = false;
        if($scope.state === "play") {
        document.getElementById('background').style.background = getQueryVariable("background");
        $scope.selectImage = function(index) {
            $scope.index = index;
            $scope.state = "pause";
        $scope.control = function(direction) {
            switch(direction) {
                case "play":
                    $scope.state = "play";
                case "pause":
                    $scope.state = "pause";
        $scope.options = getQueryVariable("options");
        if($scope.options === "{$options}") $scope.options = "yes";

Styling for the caption box

.carousel-container { position: relative; z-index: 1; float: right; margin: 0 1em 1em 1em; }
.carousel-container.left { float: left; } { float: none; clear: both; margin: 0 auto 1em auto; }
.carousel-container iframe { position: relative; z-index: 2; width: 334px; border: none; }
.carousel-container .carousel-caption { position: relative; background-color: #eee; border: solid 1px #666; padding: 2px 0; font-size: 80%; font-weight: bold; text-align: center; width: 300px; margin-top: -3px; box-shadow: 0 1px 6px rgba(0,0,0,.25); z-index: 3; max-width: 675px; }
#page-content .carousel-container .carousel-caption-wrapper { max-width: 673px; }
.carousel-container .carousel-caption-wrapper { padding: 0 16px; }
.carousel-container .carousel-caption p { margin: 0; padding: 0 10px; }
.carousel-container .carousel-caption.true { display: none; }

Styling for the rest of the carousel

    html { width: calc(100% - 32px); height: calc(100% - 4px); margin: 0; padding: 0; }
    body { width: 100%; height: 100%; margin: 0; padding: 2px 16px; background: transparent; }
    .wrapper { position: relative; width: 100%; height: 100%; }
    .carousel { position: relative; width: calc(100% - 2px); height: calc(100% - 2px); overflow-x: hidden; border: 1px solid #666; box-shadow: 0 1px 6px rgba(0,0,0,.25); box-sizing: content-box; }
    .horsie { position: absolute; height: 100%; width: 100%; top: 0; left: 0; transform: translate(0,0); transition: transform 0.3s ease-in-out; }
    .horsie img { object-fit: contain; width: 100%; height: 100%; }
    .horsie.past { transform: translate(-100%,0); }
    .horsie.future { transform: translate(100%,0); }
    .arrow { height: 30px; width: 30px; border: 1px solid #666; border-radius: 50%; position: absolute; top: 50%; background: #eee; box-shadow: 0 1px 6px rgba(0,0,0,.25); cursor: pointer; }
    .arrow .image { height: 30px; width: 30px;position: absolute; top: 0; left: 0; background-position: 50% 50%; background-size: 80% 80%; background-repeat: no-repeat; opacity: 0.6; transition: opacity 0.1s ease-in-out; }
    .arrow.inactive { cursor: default; }
    .arrow.inactive .image { opacity: 0; }
    .decrementor { left: 0; transform: translate(-50%,-50%); }
    .decrementor .image { background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACQAAAAkCAQAAABLCVATAAAAdElEQVR4AezQJwKAMBQD0DCOjAfD9j1BXVUdR0OxkWzCJvHvD/x5aVxEDMZBWVfAIDB1JQwOY96IUSzGYjCaxdgMJuMwOQJ4c51jlnc0HgsCUs5pPbX82csozaQsFqVuSpksSjIpg0OJBdACqlorw7AEowAAblWUrl8sD5AAAAAASUVORK5CYII='); }
    .incrementor { right: 0; transform: translate(50%,-50%); }
    .incrementor .image { background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACQAAAAkCAQAAABLCVATAAAAcUlEQVR4Ae3QpwJAUACF4WM+sU6xV9M0TdI8mmR389j+07878PfSHOjYnYAETTdtP5Sh4VAik8pZlMSkChYlM6mSRalLKWN2FqolVLNqGgsyOE/zOZ+9i5FZTMFgJBaT34gRWUzGYAABKYMZizvm75W1TreU8DMmtioAAAAASUVORK5CYII='); }
    .bubble-holder { display: flex; position: absolute; width: calc(100% - 60px); height: 20%; justify-content: center; align-items: flex-end; flex-wrap: wrap; align-content: flex-end; bottom: 0; margin: 5px 0; left: 30px; }
    .bubble { border: 2px solid #666; height: 0; width: 0; margin: 5px; border-radius: 50%; transition: all 0.2s ease-in-out; background-color: white; }
    .bubble.present { height: 2px; width: 2px; background-color: white; margin: 4px; }
    .bubble-holder:hover .bubble { height: 6px; width: 6px; margin: 2px; cursor: pointer; }
    .bubble-holder:hover .bubble.present { height: 12px; width: 12px; margin: -1px; }
    .control { position: absolute; height: 10px; width: 10px; left: 5px; bottom: 5px; background-size: contain; opacity: 0.3; cursor: pointer; } { opacity: 1; cursor: default; } { background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACQAAAAkCAQAAABLCVATAAAAgElEQVR4Ae2UQRWAIBBEJwIRjGAEI9hEG0ATaaINtIE0kAbqnrh4/M/Tzr//B7A78nzF0wvKraiAiF5ODYzIWBQIkXFpZETGqo4R2bkmRmTs6hmRERUIURsKQNSGAhBlQlSYqyXisQ/i+6tmYiA3YkUqs7SZqJHCFFtS+Lf8PZ4HilOsPBFiYmoAAAAASUVORK5CYII='); }
    .control.pause { left: 20px; background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACQAAAAkCAYAAADhAJiYAAAAOUlEQVR4Ae3WoQ0AMAwDwY7ezdsFzKyg3EthBgdzJCl309X7opev3wMBAQEtAQEBAQEBAU09+ZL0AdrO+xGDE6h0AAAAAElFTkSuQmCC'); }
    .invisible { display: none; }

fix the width of the caption on mobile
little dots that show you how many images there are
option to have them/not have them
click on them to go to an image
a button that re-enables the interval after it's been disabled (fade-out pause/play in bottom corner?
option to import css from url

Unless otherwise stated, the content of this page is licensed under Creative Commons Attribution-ShareAlike 3.0 License