# Custom Splash HTML Builder

**URL:** https://meta.discourse.org/t/custom-splash-html-builder/409871
**Category:** Plugin
**Tags:** splash
**Created:** [August 12, 2026, 1:04am UTC](https://meta.discourse.org/t/custom-splash-html-builder/409871 "2026-08-12T01:04:03Z")
**Posts on this page:** 3
**Page:** 1

<div class="post-metadata">

### Author: ![Don](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/don/32/228726_2.png) [@Don](https://meta.discourse.org/u/Don)
#### Post date: [August 12, 2026, 1:04am UTC](https://meta.discourse.org/t/custom-splash-html-builder/409871/1 "2026-08-12T01:04:04Z")

</div>

| | | |
| --- | --- | --- |
| ℹ | **Summary** | Discourse plugin that allows the splash screen to be customized using admin-defined HTML and CSS. |
| 🛠 | **Repository Link** | [https://github.com/VaperinaDEV/custom-splash-html-builder](https://github.com/VaperinaDEV/custom-splash-html-builder) |
| ❤ | **Found it useful?** | [\> ./support --coffee](https://meta.discourse.org/u/don) |
| 📖 | **Install Guide** | [How to install plugins in Discourse](https://meta.discourse.org/t/install-plugins-in-discourse/19157) |

  

Hello 👋

I’ve created a small Discourse plugin that allows the splash screen to be customized using **admin-defined HTML and CSS** , without having to maintain a modified copy of Discourse’s core splash template.

The original motivation for creating this plugin was actually **mobile performance**.

I wanted to create a more sophisticated animated splash screen, but I found that the SVG animations supported by the current Discourse core splash implementation could become surprisingly problematic on mobile devices.

On desktop, the animation could look perfectly smooth, while on mobile it could become noticeably choppy, drop frames, lag, or even appear to stop during the animation.

After experimenting with different approaches, I found that moving the animation from the SVG itself to a surrounding HTML element, such as a `<div>`, made a **very significant difference**.

Instead of continuously animating the SVG contents, the SVG can remain static while the browser animates the containing HTML layer using CSS transforms.

This gives the browser a much better opportunity to handle the animation as a compositing operation using the device’s graphics hardware.

The result was a much smoother animation on mobile, without the stuttering and freezing I was seeing with the SVG-based approach.

That was the main reason this plugin was created.

**Before** (Animated SVG: The animation lagging and stopped)

**After** (Animated HTML: Smooth animation)

* * *

## The problem with animating SVGs

The original splash implementation is perfectly fine for a simple logo or relatively lightweight animation.

However, once the animation becomes more complex, SVG rendering can become expensive.

For example, an animation directly applied to an SVG or its internal elements can require the browser to repeatedly process or repaint parts of the SVG during the animation.

On mobile devices, this can become particularly noticeable.

During testing, I saw cases where the animation would:

- become visibly choppy
- temporarily freeze
- appear to stop
- behave significantly worse than on desktop

The interesting part was that the same visual animation could behave very differently depending on **what was actually being animated**.

* * *

# Moving the animation to an HTML layer

The approach that worked much better was to keep the SVG itself static and put it inside a normal HTML element.

For example:

```html
<div class="logo-layer">
  <svg viewBox="0 0 500 500">
    ...
  </svg>
</div>

```

Instead of animating the SVG, the animation is applied to the container:

```css
#d-splash .logo-layer {
  animation: pulse 1.8s ease-in-out infinite;
  will-change: transform;
}

@keyframes pulse {
  0%,
  100% {
    transform: scale(0.8);
  }

  50% {
    transform: scale(0.85);
  }
}

```

The SVG itself doesn’t change.

The browser can therefore handle the transformation of the HTML layer much more efficiently, and in supported cases promote it to a composited layer handled by the graphics hardware.

This produced a **dramatically smoother result on mobile devices**.

The important distinction is therefore:

```plaintext
Core approach:

SVG
 └── SVG animation
      └── SVG contents are animated

```

vs:

```plaintext
Custom approach:

HTML layer
 └── SVG
      └── CSS transform on HTML layer
           └── compositor-friendly animation

```

This isn’t a guarantee that every animation will be GPU-accelerated. The browser ultimately decides how an animation is composited, but in my testing the difference was very noticeable.

* * *

# Why I created the Custom Splash HTML Builder

Once I had this approach working, I also needed a way to actually build the splash screen around it.

The standard splash template doesn’t provide enough flexibility for this type of implementation.

For a more complex animation, I might need:

- multiple SVG layers
- multiple HTML containers
- independently animated elements
- custom CSS keyframes
- different animation timings
- custom positioning
- completely different markup from the default splash

So instead of creating another hard-coded splash implementation, I decided to expose the visual part through two site settings.

The plugin adds:

### `splash_custom_html`

The HTML/SVG markup rendered inside the splash screen.

### `splash_custom_css`

The CSS used by the custom splash, including animations, keyframes, positioning and responsive behavior.

This makes the splash effectively customizable without having to modify the plugin source code every time the animation changes.

* * *

# Built-in admin editor

The plugin also provides a small built-in admin editor for managing the custom splash.

It adds a dedicated **Splash HTML Builder** section in the Discourse admin interface, with separate editors for:

- **Custom HTML**
- **Custom CSS**

The changes can be saved directly from the admin interface without manually editing the corresponding site settings.

The underlying settings are still:

- `splash_custom_html`
- `splash_custom_css`

The editor is simply a more convenient interface for managing them.

This also means that the plugin doesn’t require modifying plugin source files whenever the splash animation needs to be changed.

* * *

# Example

A custom splash can contain multiple independent layers:

```html
<div class="ring-layer">
  <svg viewBox="0 0 500 500">
    ...
  </svg>
</div>

<div class="logo-layer">
  <svg viewBox="0 0 500 500">
    ...
  </svg>
</div>

```

And each layer can have its own animation:

```css
#d-splash .ring-layer {
  animation: rotate 2.2s linear infinite;
  will-change: transform;
}

#d-splash .logo-layer {
  animation: pulse 1.8s ease-in-out infinite;
  will-change: transform;
}

@keyframes rotate {
  from {
    transform: rotate(0deg);
  }

  to {
    transform: rotate(360deg);
  }
}

@keyframes pulse {
  0%,
  100% {
    transform: scale(0.8);
  }

  50% {
    transform: scale(0.85);
  }
}

```

The SVGs remain static while the surrounding HTML layers are animated.

This makes it possible to create considerably more complex splash animations while keeping the expensive animation work outside the SVG itself.

* * *

# Why not simply override the core splash template?

Another important goal was to avoid maintaining a copy of Discourse’s core splash template.

A straightforward approach would be to override:

```plaintext
app/views/common/_discourse_splash.html.erb

```

and copy the current Discourse implementation into the plugin.

The problem is that this creates a maintenance burden.

If Discourse changes its splash implementation in a future release, the plugin would still contain the old version.

That could potentially result in:

- missing new core changes
- missing performance improvements
- broken behavior after a Discourse update
- having to manually compare the plugin template with core after every update

I wanted to avoid that entirely.

* * *

# Core fallback

The plugin therefore supports a custom splash with a core fallback.

### Custom HTML is configured

If:

```plaintext
SiteSetting.splash_custom_html.present?

```

then the plugin renders the custom splash.

### Custom HTML is empty

If no custom splash has been configured, the plugin falls back to the **current Discourse core splash template**.

The plugin locates the actual core file from the running Discourse installation:

```plaintext
Rails.root/app/views/common/_discourse_splash.html.erb

```

and renders that implementation.

Conceptually:

```ruby
core_splash_path = Rails.root.join("app", "views", "common", "_discourse_splash.html.erb")

if File.exist?(core_splash_path)
  render inline: File.read(core_splash_path), type: :erb
end

```

This means the plugin does **not** carry a second copy of the core splash template.

* * *

# Performance considerations

The plugin isn’t trying to claim that every CSS animation will magically become GPU accelerated.

The browser still decides how individual animations are rendered and composited.

The goal is instead to give the browser a much more favorable structure for hardware-accelerated compositing:

- keep SVG content static
- isolate independently animated elements
- animate HTML layers
- prefer `transform` for movement/scaling/rotation
- avoid unnecessarily expensive repaint operations
- use `will-change` where appropriate

For example:

```css
#d-splash .ring-layer {
  will-change: transform;
  animation: rotate 2.2s linear infinite;
}

```

This approach worked particularly well for my use case and eliminated the mobile stuttering I was seeing with the original SVG animation.

* * *

# Enable or disable the custom splash

The plugin also provides a `custom_splash_html_builder_enabled` site setting.

When disabled, the standard Discourse splash screen is used regardless of whether custom HTML or CSS has been configured.

This provides an additional safety switch for temporarily disabling the custom splash without deleting the saved HTML/CSS.

The custom splash is only rendered when both:

```plaintext
custom_splash_html_builder_enabled = true
splash_custom_html is not empty

```

Otherwise, the current Discourse core splash is used.

* * *

Most importantly, it provides a way to build a custom animated splash that performs much better on mobile by **animating HTML layers around static SVG content instead of directly animating the SVG itself**.

---

<div class="post-metadata">

### Author: ![Don](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/don/32/228726_2.png) [@Don](https://meta.discourse.org/u/Don)
#### Post date: [August 13, 2026, 1:46pm UTC](https://meta.discourse.org/t/custom-splash-html-builder/409871/2 "2026-08-13T13:46:25Z")

</div>

Hello 👋

I’ve added `data-color-scheme` to the `#d-splash` section, so you can easily set up light and dark scheme splash logos. [DEV: Implement dynamic color scheme for splash section · VaperinaDEV/custom-splash-html-builder@ba6641b · GitHub](https://github.com/VaperinaDEV/custom-splash-html-builder/commit/ba6641b54c550b6d84ffac0251e7045cee95fe9c)

```erb
<%- splash_forced_scheme = (dark_color_scheme? || forced_dark_mode?) ? "dark" : (forced_light_mode? ? "light" : nil) %>

<section id="d-splash"<%= " data-color-scheme=\"#{splash_forced_scheme}\"".html_safe if splash_forced_scheme %>>

```

So `#d-splash` gets a `data-color-scheme="dark"` / `"light"` attribute when a light or dark scheme is explicitly forced, and is left without the attribute only when both schemes are enabled and the OS is meant to decide.

The fix is to let that attribute take priority over the media query in the custom CSS, and only fall back to `prefers-color-scheme` when the attribute is absent.

Example:

**Custom HTML**

```html
<!-- LIGHT MODE -->
<div class="custom-splash-light">
  <div class="ring-layer">
    <svg viewBox="0 0 500 500">
      ...
    </svg>
  </div>
    
  <div class="logo-layer">
    <svg viewBox="0 0 500 500">
      ...
    </svg>
  </div>
</div>

<!-- DARK MODE -->
<div class="custom-splash-dark">
  <div class="ring-layer">
    <svg viewBox="0 0 500 500">
      ...
    </svg>
  </div>
    
  <div class="logo-layer">
    <svg viewBox="0 0 500 500">
      ...
    </svg>
  </div>
</div>

```

**Custom CSS**

```css
#d-splash .custom-splash-dark {
  display: none;
}

/* OS decides — only when there's no forced scheme */
@media (prefers-color-scheme: dark) {
  #d-splash:not([data-color-scheme]) .custom-splash-light {
    display: none;
  }
  #d-splash:not([data-color-scheme]) .custom-splash-dark {
    display: block;
  }
}

/* forced scheme always wins, regardless of OS */
#d-splash[data-color-scheme="light"] .custom-splash-dark {
  display: none;
}
#d-splash[data-color-scheme="dark"] .custom-splash-light {
  display: none;
}
#d-splash[data-color-scheme="dark"] .custom-splash-dark {
  display: block;
}

```

Since `:not([data-color-scheme])` simply doesn’t match once the attribute is present, there’s no specificity fight between the two rule sets, the forced scheme always wins.

---

<div class="post-metadata">

### Author: ![agemo](https://avatars.discourse-cdn.com/v4/letter/a/ac91a4/32.png) [@agemo](https://meta.discourse.org/u/agemo)
#### Post date: [August 13, 2026, 2:18pm UTC](https://meta.discourse.org/t/custom-splash-html-builder/409871/3 "2026-08-13T14:18:19Z")

</div>

Cool this is something I was suggesting / looking for years ago, to control branding/image when connection is slow, so the user is more orientated and anchored when in limbo.

On first look this is even more than I imagined. Look forward to trying it out at some point. Great work! Thanks.
