Invision Community 5 renders in a light or a dark scheme. Custom CSS written while looking at one of them usually breaks in the other, and the way it breaks is quiet: nothing errors, nothing logs, and the developer's own screen looks correct.
How the two schemes work
Core declares its palette twice: light values at :root, then overrides under [data-ips-scheme="dark"]. The attribute is written onto <html> from front/global/htmlDataAttributes.phtml and can be light, dark, or system. When it is system, script resolves the OS preference and rewrites the attribute; data-ips-scheme-active keeps the unresolved choice.
So light is the default and dark is the override — not the other way round.
🚨 The trap: a wrong variable name is not an error
This is the one that will cost you a day.
/* Looks scheme-aware. Is not. */ background: var(--i-box--background, #171b21);
--i-box--background does not exist. The correct name is --i-box--ba-co. When a custom property is undefined, var() quietly uses the fallback — so this rule paints #171b21 in both schemes. On dark it looks perfect. On light you get black cards on a white page, and because you wrote a fallback you will be certain the rule is scheme-aware and go looking somewhere else.
The abbreviations are not guessable, which is exactly why this happens. Verified names:
--i-box--ba-co | box background |
--i-box--bo-co | box border colour |
--i-box--bo-ra | box radius |
--i-boxHeader--ba-co | box header background |
--i-background_1 … --i-background_6 | surface ramp, 1 = page, 6 = strongest |
--i-color_hard / --i-color_soft | primary / secondary text |
--i-color_primary, --i-primary | accent |
--i-design-radius | theme radius setting |
Names that look right and are wrong: --i-box--background, --i-box--border, --i-border, --i-background-color_1, --i-background-color_2.
Check before you rely on one. In the browser console:
getComputedStyle(document.documentElement).getPropertyValue('--i-box--ba-co')
// empty string means the property does not exist and your fallback is what renders
Worth a build-time sweep too: grep your CSS for --i-[a-z0-9_-]+ and compare against a known-good list. Finding this by eye across a catalogue of applications does not happen.
Declare your own tokens, twice
Rather than duplicating every rule per scheme, mirror what core does — but with your own prefix. Never redefine core's --i-* variables at :root; every component that depends on them changes underneath you.
:root{
/* On light a "raised surface" highlight has to be a DARK tint — a white
highlight on a white card is invisible. */
--ed-lift: rgba(15, 23, 42, .035);
--ed-hairline: rgba(15, 23, 42, .12);
/* Light needs far less shadow opacity or it reads as soot. */
--ed-shadow: rgba(15, 23, 42, .16);
--ed-art-bg: #e9eef4;
--ed-code-bg: #f6f8fa;
}
[data-ips-scheme="dark"]{
--ed-lift: rgba(255, 255, 255, .055);
--ed-hairline: rgba(255, 255, 255, .10);
--ed-shadow: rgba(0, 0, 0, .85);
--ed-art-bg: #0f141b;
--ed-code-bg: #0d1117;
}
Two habits worth keeping: rgba(255,255,255,x) for hairlines and highlights is a dark-only assumption, and heavy rgba(0,0,0,.85) shadows that read as depth on dark read as dirt on light.
Not everything should follow the scheme
Some surfaces carry their own ground and must stay fixed. Tokenising these is a regression, not a fix:
- A header band that is always a brand gradient — white text on it is correct in both schemes.
- A chip or badge sitting on top of a photograph or cover art, with its own dark backing.
- A darkening tint over a header photograph. Lighten it in light mode and the texture washes out and the logo stops standing off the background.
Comment those, or the next person to "fix" the scheme handling will flatten them.
Testing it: the attribute is set once
🚨 data-ips-scheme is written at page load. Changing your OS scheme in another window does not update it — the stale value stays and your CSS appears broken when it is fine. Reload after every scheme change.
Also: computed values are not verification. Confirming that --ed-art-bg resolved to the light value proves the token works, not that the page looks right — a single un-tokenised rule elsewhere still ruins it. Look at the rendered page in both schemes.
Writing custom CSS from code
🚨 $theme->custom_css = $css; $theme->save(); is a silent no-op. The setter writes to _data while save() only persists $this->changed. Write the column and clear the compiled copy:
\IPS\Db::i()->update( 'core_themes', [ 'set_custom_css' => $css ], [ 'set_id=?', $id ] ); $key = $id . '_theme_editor_custom_css'; unset( \IPS\Data\Store::i()->$key );
Bonus: the 1px white seam on hover
A card with overflow: hidden, a border-radius, and a transform on hover, containing an image that also transforms, flashes a hairline of the page through its rounded corners. The rounded clip is re-rasterised on the card's compositing layer and leaks an antialiased edge.
🚨 The instinctive cures — transform: translateZ(0), will-change: transform — make it worse, because promoting the element to its own layer is what causes the clip to be rasterised separately in the first place.
Fix it geometrically instead: never let the image's edge sit at the clipping boundary.
.card{ overflow: clip; isolation: isolate; }
/* clip on the image's OWN container, which never transforms, with the radius
matched to the card's (16px card minus its 1px border) */
.card__image{ overflow: clip; border-radius: 15px 15px 0 0; }
/* resting overscan: invisible on a photo, but the edge already sits outside
the clip before the animation starts */
.card__image img{ transform: scale(1.02); }
.card:hover .card__image img{ transform: scale(1.08); }
overflow: clip is preferable to hidden here: it does not create a scroll container and rasterises more predictably. Note also that a hover box-shadow containing a hard 0 0 0 1px ring is itself indistinguishable from a flash — check for one before blaming the clip.
Verified against
Invision Community 5.0.19, variable names read from the live computed styles of a running community and from applications/core/dev/css/global/framework/1-3-variables.css and 1-4-colors.css.
Recommended Comments