Skip to main content

background-contrast

Default opacity and scrim values for pages and app UI that sit on a photographic or video background. Use whenever a background image is "barely visible" or washed out, when text over artwork needs contrast, when adding a hero/plate image behind a layout, or when sidebars, panels and cards need to read against a busy backdrop. Carries the compounding-gradient trap and the arithmetic to check any stack.

Informations de source

Dépôt
Violinet-tech/background-contrast
Dernière activité de la source
5 octobre 2026 à 18:48
Langue détectée de SKILL.md
anglais
Étoiles
0
Forks
0

Options d'installation

Le prompt qui vérifie d'abord la source est sélectionné par défaut. Vous pouvez passer à une commande directe ou télécharger une copie locale.

Vérifiez les fichiers source

Lisez SKILL.md et les fichiers associés affichés par SkillsMP avant de décider de l'installer.

Affichage de SKILL.md

SKILL.md
Instructions source · Aperçu en lecture seule
name
background-contrast
description
Default opacity and scrim values for pages and app UI that sit on a photographic or video background. Use whenever a background image is "barely visible" or washed out, when text over artwork needs contrast, when adding a hero/plate image behind a layout, or when sidebars, panels and cards need to read against a busy backdrop. Carries the compounding-gradient trap and the arithmetic to check any stack.
# Backgrounds you can actually see The failure this exists to prevent: buying a background, covering it to make text legible, and shipping a page where the artwork is invisible. It has happened twice in this workspace, both times because **stacked translucent layers compound and the numbers were never worked out**. ## The arithmetic, which is the whole skill Opacities multiply; they do not add. For a plate at opacity `p` under scrims of opacity `s1, s2, …`: ``` visible fraction of the artwork = p x (1 - s1) x (1 - s2) x ... ``` Two real cases from this workspace: ``` radial .86 over linear .93 -> 1 - (0.14 x 0.07) = 99% opaque plate .34 under a .86-.93 scrim -> artwork visible at 2.4 - 6.8% ``` Both looked reasonable as individual numbers. Neither was. **Always compute the product before shipping.** One line: ```python p, scrims = 0.55, [0.58] print(f"{p * math.prod(1 - s for s in scrims) * 100:.0f}% visible") ``` ## Defaults | | value | notes | |---|---|---| | **Artwork visible** | **25–45%** | the target the numbers above must land in | | Full-page scrim | **0.28 top → 0.62 bottom** | one gradient, not two stacked | | Plate opacity (app UI) | **0.55** | when the plate is a separate layer | | Local pool behind bare text | 0.55–0.60 radial | spend darkness *here*, not everywhere | | Panels / cards over artwork | **0.80–0.90** | they carry their own ground | | Sticky nav / sidebar | 0.90 → 0.42 gradient + `backdrop-filter: blur(14px)` | | | Text shadow, bare text only | `0 2px 26px rgba(bg,.85)` | display type; `0 1px 16px` for body | ## The principle **Win contrast locally, not globally.** A full-page wash is the blunt fix: it costs the artwork everywhere to solve a problem that exists in two places. 1. Keep the global scrim light. 2. Put a **soft radial pool** behind the one or two runs of text that sit on bare artwork (usually the hero copy). 3. Everything else already has a panel — raise *those* backgrounds instead. 4. Add a text-shadow to display type as the last 10%. ## Applying it ```css /* the plate: a still or a looping video */ .plate{ position:fixed; inset:0; z-index:-3; pointer-events:none; width:100%; height:100%; object-fit:cover; background:url('bg.webp') center/cover no-repeat; } /* ONE scrim. Darkness concentrated where bare text lands. */ .scrim{ position:fixed; inset:0; z-index:-1; pointer-events:none; background: radial-gradient(75% 55% at 26% 34%, rgba(6,9,26,.58), transparent 72%), linear-gradient(180deg, rgba(6,9,26,.28) 0%, rgba(6,9,26,.40) 48%, rgba(6,9,26,.62) 100%); } ``` That radial-over-linear pair compounds to 68% only where the hero copy sits and 28–62% everywhere else — which is the point of splitting them. ## Checking it Contrast still has to pass: **4.5:1 for body text, 3:1 at 24px+** (5.5:1 is a better floor in practice — see the stray-overlay story below, where 4.91:1 passed AA and was still too dim at 11px). Over a background the worst case is one of the artwork's two EXTREMES composited under the panel — which extreme depends on which way the palette runs, and checking only one is a trap: - **Light text on a dark panel** loses contrast where the artwork is **brightest** (a light source bleeding through drags the surface up toward the text). - **Dark text on a light panel** loses it where the artwork is **darkest** (a dark pixel drags the surface down toward the text). A check written for one direction and never revisited becomes silently wrong the moment a palette flips — it would pass the exact failure it exists to catch. Check both extremes (composite pure white AND pure black under the panel at its real alpha) and take whichever is worse; don't assume. A good place to put this is a small script run in CI or `npm run check:contrast` that covers token-on-panel pairs: a direction-agnostic worst-case check, plus a literal-rgba assertion so a `.panel` rule can't silently drift from its own token. It does **not** cover text sitting directly on bare backdrop with no panel under it — do that arithmetic by hand. ## Before touching the palette, rule out a stray overlay **"No opacity" is not always a contrast problem.** One app's dark palette was rebuilt, failed, and reverted to light four times before the real bug was found: `#modal { display: flex }` outranked `.hidden { display: none }` (an ID selector beats a class one), so the approval overlay stayed painted over the whole app on every one of those runs — a 70%-black wash greying every panel, which looks *identical* to a genuine value-separation failure whether or not the palette underneath is sound. Four rounds of retuning scrim and panel numbers never touched the actual bug, because it wasn't in the palette. A token-pair contrast check cannot see this class of bug — it only reads CSS custom properties, not runtime state, a stray full-screen element, or a hardcoded fill that never used a token at all. Two other real examples from this workspace: `calc()` inside `color-mix()` computed correctly under `getComputedStyle` and painted nothing, and `backdrop-filter` does the same under software compositing — both leave a panel fully transparent while every tool reports the palette as applied. Before redesigning contrast values in response to a "washed out" or "no opacity" report, rule out: a modal/overlay with a specificity bug, a hardcoded (non-token) fill, and a CSS feature that silently no-ops under the current renderer. Only redo the arithmetic once you've confirmed the palette is what's actually on screen. Related: `seamless-loop-video` for when the plate is a video. Remove any CSS drift from a plate that moves on its own.
Voir sur GitHub