--- name: compose-visuals-footguns description: "Compose look-and-feel traps: theme and colour tokens, artwork palettes, gradients and scrims, shimmer, glass and particle effects, animation, image loading, charts and stats displays (percent changes, shares, buckets). Use when a visual bands, pops, freezes or misleads." --- # Compose visuals footguns: theming, effects and charts 35 traps mined from a production Kotlin and Compose Multiplatform app, one short file each under `references/`. Every file gives the working pattern, the **Traps** (the specific ways it fails in practice, and why), and a **Verifying it** section with commands to run against the user's own tree. **How to use this index.** Match the code about to be written, or the symptom being chased, against the entries below: each names its topic and the symptom that should send you to it. Read every file that plausibly applies with the Read tool before proposing code or a fix; each is under 150 lines. Where a Verifying command names the source project's paths, substitute the equivalent paths in the user's tree. **Cross-references.** A backticked trap name inside a file that is not listed here lives in a sibling area skill: glob `../*/references/.md` to open it. ## Theming, colour, gradients and scrims - [`smooth-scrim-gradient`](references/smooth-scrim-gradient.md) — Build a scrim that melts artwork into the page background without a visible seam — smoothstep easing so the ramp is flat at both ends, colour stops interpolated in Kotlin rather than left to the renderer, and a transparent stop that carries your own RGB instead of Color.Transparent. Use when a gradient overlay shows a hard line where it starts or ends, when the middle of a fade turns muddy grey or darker than either end, or when a fade that looks right on one platform bands into stripes on another. - [`angled-gradient-modifier`](references/angled-gradient-modifier.md) — Draw a linear gradient at an arbitrary angle across a Compose box so both endpoints land exactly on the box edge — the per-quadrant endpoint formula from the requested angle, why rotating the diagonal overshoots and why clamping to the nearest edge distorts the angle, and the degenerate cases that collapse the ramp to nothing. Use when a tilted gradient looks washed out or cut off near the corners, when the visible angle does not match the angle you asked for, or when the same gradient looks different on a wide box than on a tall one. - [`artwork-palette-theming`](references/artwork-palette-theming.md) — Drive a screen's colours from its artwork — which extracted swatch to use for an accent versus for a large page background, luminance-adaptive darkening so overlaid text stays readable on any image, the hex parsing helper this needs, and what to do when extraction returns nothing or there is no artwork at all. Use when a page background comes out lurid or unreadably light, when it flashes or changes while scrolling a list, or when a screen renders transparent or invisible instead of tinted. - [`force-dark-immersive-subtree`](references/force-dark-immersive-subtree.md) — Keep one part of a Compose app rendered dark — screens drawn over dark artwork — while the rest of the app follows the user's light theme, by providing a whole dark colour scheme plus your own token locals to that subtree; includes why a text-colour flag alone is not enough, and the verified fact that bottom sheets and dialogs inherit these locals across the window boundary while a freshly rooted composition does not. Use when icons or buttons on an image-backed screen turn grey and unreadable in light theme, when a sheet opened from such a screen comes out the wrong colour, or when a forced-dark screen still asks the system whether it is night. - [`semantic-color-tokens-compositionlocal`](references/semantic-color-tokens-compositionlocal.md) — Hold the colours that have no Material role — a liked-state pink, an active-line highlight, shimmer tones, overlays that sit on artwork — in an @Immutable token class provided through staticCompositionLocalOf, with the bytecode-level reason static is the right choice for theme values and a rule for what belongs in the token class versus in the colour scheme. Use when hex literals are spreading through composables, when a colour has to differ between light and dark but is not a scheme role, or when you are choosing between staticCompositionLocalOf and compositionLocalOf. - [`deterministic-title-placeholder-painter`](references/deterministic-title-placeholder-painter.md) — Give items with no artwork a cover of their own by hashing the title into a stable gradient and packaging it, plus measured text and a badge, as a custom Painter you can hand straight to an image loader's placeholder, error and fallback slots — covering what makes the hash actually deterministic, measuring text outside a layout pass, and reporting an intrinsic size. Use when coverless rows all look identical, when a generated colour changes between runs or platforms, when placeholder text spills outside its tile, or when a null image model leaves a blank square. - [`overflow-tilted-browse-card`](references/overflow-tilted-browse-card.md) — Build a browse tile whose cover art is tilted and runs off the clipped corner — the modifier order that makes the diagonal a cut rather than a pasted square, the rotated bounding-box growth that decides how much room siblings must leave, and why a non-square tile cannot size its decoration off the width. Use when a rotated image shows sliced corners, when text collides with tilted art or reflows the moment that art loads, or when a rotated child covers its neighbours instead of being clipped by its parent. - [`artwork-seeded-dynamic-scheme`](references/artwork-seeded-dynamic-scheme.md) — Derive a complete tonal colour scheme from one artwork-extracted seed and wrap only that subtree in it, so every control inherits contrast-paired roles instead of hand-picked swatches — covers falling back by comparing against the sentinel the seed was initialised with, tweening the seed rather than the scheme, and the cost of re-deriving per animation frame. Use when a screen should recolour itself to its artwork, when foreground text on a tinted surface comes out unreadable, or when a colour change lands as a visible flash. - [`palette-state-parks-on-loading`](references/palette-state-parks-on-loading.md) — A palette generator that flips its state to Loading before its own suspension point reports no colour for the entire duration of every generation, and a generation cancelled part-way leaves it Loading with nothing to restart it — so any surface reading the palette directly paints its null fallback. Covers why the effect must be keyed on the bitmap alone (or on nothing at all), why a "already done" flag assigned after a suspension is not a record of done, and why the last resolved colour has to be held separately. Use when a screen tinted from artwork is sometimes right and sometimes black, when the same item tints correctly on one visit and not the next, or when a derived colour scheme silently sits on its fallback. - [`gate-optional-effect-at-the-shared-primitive`](references/gate-optional-effect-at-the-shared-primitive.md) — Put a user's on/off setting for an optional decorative surface treatment inside the one shared primitive that draws it — published from the theme as a CompositionLocal defaulting to on — instead of asking every call site to check the flag. Covers why the off path must keep the shape and the hit target and change only the paint, why the default is true rather than false, and the difference between a call site that picks a material and one that picks a different composable. Use when a settings toggle reaches only some of the surfaces it names, when a gated component renders as a bare box in previews, or when turning an effect off also moves the layout. - [`shell-background-is-not-scheme-background`](references/shell-background-is-not-scheme-background.md) — The moment an app shell paints its content panels in anything other than colorScheme.background, every gradient, scrim and fade whose tail converges on colorScheme.background ends on a hard seam — the decoration is still correct, its destination colour is just no longer on screen. Covers resolving one page-background value and handing it to every decoration, the two valid answers (aim at the shell colour, or paint your own ground), and sizing a decoration with matchParentSize over the content it decorates rather than a constant measured on one form factor. Use when a gradient stops mid-screen with a visible edge, when a fade looks right on a phone and wrong in a desktop window, or when adding a window chrome breaks screens that were never touched. - [`flat-twin-shares-geometry-not-material`](references/flat-twin-shares-geometry-not-material.md) — When a user setting picks between two renderings of the same control — an expensive decorative one and a plain twin — every geometry decision has to be mirrored between them (item width, bar height, indicator size, inset, and the rule that computes the width budget) so toggling changes the material and never the layout; the compiler cannot pair two constants declared in two files. Also covers recolouring an icon that arrives as a slot lambda, which only CompositionLocalProvider(LocalContentColor) can reach. Use when switching a visual setting also moves things, when the plain variant's indicator sits off its item, or when a slot-lambda icon ignores every tint you pass. - [`chrome-drawn-outside-the-theme-scope`](references/chrome-drawn-outside-the-theme-scope.md) — A custom window title bar, splash, or crash dialog composed as a sibling of the app theme rather than inside it reads MaterialTheme's framework DEFAULT scheme — light, always, no matter what the user picked — and nothing errors. Covers extracting the stored-mode-to-boolean decision as one shared composable function so the chrome and the theme cannot answer differently, passing colours in as parameters instead of re-theming, and the colours that must deliberately not follow the theme at all. Use when a title bar or dialog stays light in dark mode, when chrome colours lag one launch behind the setting, or before adding any composable above the theme call. - [`ambient-tone-layer-behind-flat-pages`](references/ambient-tone-layer-behind-flat-pages.md) — A reusable top-glow layer that gives pages with no imagery of their own the same tinted ground the image-backed screens get — emitted as the first sibling of a navigation destination's content, with no wrapper, because destinations already stack. Covers why a null tone must collapse the gradient into the page colour instead of substituting a theme colour, why the scroll-away offset belongs in the draw phase, why the first list item must be taller than the layer, and which screens are right to keep their own copy. Use when a flat settings or list page looks unrelated to the rest of the app, when an idle app shows a glow for nothing, or when a glow snaps out of place as the list scrolls. ## Effects, animation and images - [`material-symbols-icon-system`](references/material-symbols-icon-system.md) — Ship every icon as a generated ImageVector extension property on one receiver object, fetched from the icon font's own Compose generator, so the bytecode shrinker can drop the ones you never reference. Covers the generator request and its axis parameters, the edits that turn a generated file into an extension property, the filled/unfilled pairing for state icons, and which icons must stay as drawable resources. Use when adding or replacing an icon, when a set has drifted into mismatched weights and corner styles, or when a type error reports an ImageVector where a Painter was expected. - [`liquid-glass-backdrop`](references/liquid-glass-backdrop.md) — Build refracting "liquid glass" surfaces in Compose with a backdrop library, and avoid the three failures that waste the most time — a rim highlight that is directional by default and so goes nearly invisible on small round buttons, a backdrop source nested inside the glass it feeds, which is a render-feedback loop that stops the shader, and a source with nothing in it, which renders the control as a grey coin over a flat page. Covers the source/surface split, giving a flat page a ground worth refracting rather than dropping the effect, the white default tint that only suits forced-dark screens, the effect stack, keeping the press gesture observe-only, and the swap experiment that tells a geometry problem from a placement problem. Use when a glass surface renders as a flat rounded box or a grey coin, when the rim shows on a wide pill but not on a circular button, when its glyph disappears at light theme, or when the draw pass crashes inside the shader. - [`shimmer-skeleton-loaders`](references/shimmer-skeleton-loaders.md) — A self-measuring shimmer modifier plus skeleton composables that stand in for a list while it loads — how the modifier learns its own size, why the base colour under the sweep is load-bearing, why the modifier order between clip and background changes what gets rounded, and why the skeleton's lazy lists must have scrolling switched off. Use when building loading placeholders, or when a shimmer renders as a flat block, has square corners under a rounded design, or pauses visibly between passes. - [`animated-gradient-border-ring`](references/animated-gradient-border-ring.md) — A rotating sweep-gradient ring around a card, chip or button, built from a clipped box, a full-size gradient and an opaque inset surface parked on its middle — where the modifier order is the whole mechanism and no blend mode is involved — plus why the SrcIn-inside-an-offscreen-layer version of this cannot draw a ring, and what that layer costs. Use when the gradient covers the whole surface instead of the border, when the ring is invisible, when it bleeds over whatever is behind the widget, or when a row of them makes scrolling expensive. - [`hoist-the-flag-not-the-animation`](references/hoist-the-flag-not-the-animation.md) — Share the boolean that drives a fade, never the tween that runs it — each look derives its own curve, so one can go asymmetric (fast in, slow out) without changing how the other feels; covers why a symmetric linear fade over a bright backdrop makes container-backed controls read as the wrong colour, and why the shared animated value must stay published anyway. Use when buttons look lighter or darker than their neighbours only while something is fading, when one visual style needs a different timing from another, or before pulling an `animateFloatAsState` up into a shared state holder. - [`dont-pre-animate-a-self-animating-property`](references/dont-pre-animate-a-self-animating-property.md) — Pass the raw target to a component that animates a property itself — an externally tweened value is a stream of new targets, and such components typically ignore new targets while their own animation is still running, which freezes the effect part-way. Covers how to recognise a self-animating property, why a hard flip between endpoints is the fix, and how to prove ownership from the compiled artifact. Use when an animated component sticks near its starting value, when a transition plays once and never again, or before wrapping a component's input in `animateFloatAsState`. - [`draw-outside-bounds-particle-modifier`](references/draw-outside-bounds-particle-modifier.md) — A celebration burst drawn by a plain draw modifier that paints past its host's own bounds — draw modifiers are unclipped by default, so the modifier must sit before every .clip(...) in the chain, and any clipping ancestor still trims whatever leaves its edge. Covers scaling origin and reach off the host's measured size so one effect reads the same at 28dp and at 48dp, keeping the per-frame lambda cheap, and why pure drawing beats an animation library here. Use when a burst renders cut to the button's outline, when it vanishes inside a rounded card or list row, or before adding a vector-animation dependency to draw one. - [`image-fallback-url-retry-in-composition`](references/image-fallback-url-retry-in-composition.md) — Retry a record's image at a variant the source guarantees when the high-resolution one is missing, by holding the URL in composition state and swapping it once in onError. Covers making the disk-cache key follow the mutated URL, resetting the state per record, keeping the swap a no-op the second time so it cannot loop, and the invisible cost — everything hanging off onSuccess (a palette, and the page theme derived from it) silently never runs. Use when a page keeps its fallback colour for particular records, when a retried image is re-fetched on every visit, or when an image request appears to retry forever. - [`text-brush-shimmer-sweep`](references/text-brush-shimmer-sweep.md) — Sweep a travelling highlight through a label by putting a moving gradient on the TextStyle itself — the glyphs are painted by the brush, so there is no overlay, no clip and no measured width to keep in sync. Covers declaring the infinite transition unconditionally so the sweep does not restart every time the label appears, why the sweep head must be a pure high-contrast colour rather than the label's own, why the gradient stops are pixels and must travel past both ends, and that a brush replaces the text colour outright. Use when a shimmering label jumps back to the start whenever it reappears, when the gleam is invisible against the label's own grey, or when setting a brush makes a carefully chosen text colour vanish. - [`touch-indication-bounds-and-alpha`](references/touch-indication-bounds-and-alpha.md) — Soften an app-wide touch ripple by alpha alone and give it the right bounds — the colour derives from the local content colour and stays correct inside a forced-scheme subtree, while pinning one paints darker than a near-black surface. Covers why the theme's configuration reaches a bare Modifier.clickable, why .clip(shape) must come before .clickable, why a card and its clip need one shape value, why two clickable modifiers must never stack, and the deprecated constructor that is the only way to set alpha. Use when a tap on a rounded item flashes a square, when the ripple reads as a sooty smudge on a dark theme, or when a long-pressable item ripples twice. - [`crossfade-container-sizes-to-the-visible-child`](references/crossfade-container-sizes-to-the-visible-child.md) — A crossfade's container is a Box aligned to the top-start corner and sized to the largest child currently composed, so swapping a thin child for a taller one pins the thin one to the top mid-transition and drops it when the tall one leaves. Covers boxing both branches into one fixed frame with an explicit alignment, replacing rather than stacking two progress renderers whose track lengths differ, and why fading a whole interactive control makes it untouchable. Use when a bar visibly falls into place after a state change, when two stacked tracks are different lengths, or when a control stops responding for the length of a fade. - [`slide-transition-defaults-to-half-a-height`](references/slide-transition-defaults-to-half-a-height.md) — A vertical slide transition defaults to HALF the element's height, so it appears already halfway through its own movement and the first part is missing — which reads as a pop, not a slide. Covers passing the full height, the sign that decides which edge it comes from, pairing a shorter fade with a longer slide so the element is opaque before it settles, making exit quicker than enter, and publishing the pair as shared values so every screen matches. Use when a bar or panel seems to snap into place instead of sliding, when enter and exit feel mismatched, or when the same control animates differently on two screens. - [`transparent-chip-selection-signals`](references/transparent-chip-selection-signals.md) — A see-through selection chip — a drop shadow under a transparent shape shows THROUGH it as a dark ring, and zeroing the resting elevation does NOT remove it, because each interaction state carries its own independent token default. Covers dropping the selection-only leading icon that widens the chip by 26dp and reflows a scrolling row under the finger that just tapped it, switching the outline off when the fill arrives, and pairing a role colour with its own "on" token. Use when a transparent chip has a dark halo that comes back on press or hover, when tapping a chip shifts its neighbours sideways, or when a selected chip's label is unreadable on one of the two themes. - [`word-timed-karaoke-lyrics`](references/word-timed-karaoke-lyrics.md) — A per-word karaoke wipe driven straight off a ticking time source looks stepped instead of smooth, a word that was already fully sung stays lit after the user seeks backward past it, or skinning an existing line-level lyrics renderer for word-level highlighting leaves an unsynced sheet glowing white end to end. Use when building or debugging word-by-word lyric highlighting, a synced-transcript view, or any left-to-right text "fill" effect driven by a playback clock. - [`compose-capture-to-share-image`](references/compose-capture-to-share-image.md) — Render a composable off-screen into an ImageBitmap so a button can save or share it as a picture — a capture primitive ported into common code rather than pulled in as a platform-only dependency, a max-width ceiling standing in for a fixed output size, artwork that has to already be resolved before the button is even reachable, and an Android MediaStore save gated behind a permission that the same feature's Desktop save never needs. Use when a "share as image" feature exports a blank or half-drawn picture, when the exported picture comes out a different size on every device, or when saving works on some Android versions and fails silently on others. ## Charts and statistics displays - [`self-normalised-axes-need-a-second-shape`](references/self-normalised-axes-need-a-second-shape.md) — Build a radar/fingerprint chart whose axes are normalised 0..1 from your own data — no external corpus, one guarded denominator per axis, and a second polygon (the previous period) because a lone shape on self-normalised axes says nothing. Use when a "usage personality" or "year in review" chart needs a reference it cannot get, when a thin period leaves one axis at a guarded zero beside four real readings, or when one axis goes NaN and the whole polygon disappears. - [`dont-slice-one-circle-between-unrelated-measures`](references/dont-slice-one-circle-between-unrelated-measures.md) — Put several measures that share no whole onto concentric arcs instead of slicing one circle between them, cap the largest sweep short of 360°, and draw value wedges over a full-ring track. Use when a donut/pie is about to encode counts that do not add up to anything, when the biggest ring looks identical to a full one, or when an "almost nothing here" bucket reads as a missing tick. - [`delta-absent-not-infinite`](references/delta-absent-not-infinite.md) — Render a change figure against an empty or zero baseline as nothing at all — never "+100%", never an infinity, never a saturated integer — and guard the two spans being compared as well as the divisor. Use when a new user's first period shows a huge increase against every figure, when "+0%" appears beside a number that went down, or when every delta on a screen reads as a decline for reasons nobody can explain. - [`partial-chart-must-say-so`](references/partial-chart-must-say-so.md) — Print the share of input a distribution could actually classify, computed with exactly the predicates that built the buckets, and keep that line reachable when coverage is zero. Use when a chart is built over a nullable join or a parsed text column, when the bars look plausible but the totals underneath disagree, or when the one case that most needs a disclaimer is the case that renders nothing. - [`equal-buckets-or-no-buckets`](references/equal-buckets-or-no-buckets.md) — Split a span into buckets of exactly equal width and let the remainder fall outside, pick the bucket unit from how many rows a person will read, and never draw a partial newest bucket at full width. Use when a bar chart's oldest or newest bar is inexplicably long or short, when a range produces thirty rows nobody reads, or when one range in a set renders in the opposite direction from the others. - [`mosaic-arrangements-must-be-hole-free`](references/mosaic-arrangements-must-be-hole-free.md) — Build a ranked mosaic (one big tile plus smaller ones) with an arm for every possible count, so no entry is silently dropped and no arrangement leaves an empty rectangle — plus one clip around the whole block rather than one per tile. Use when a "top five" shows four, when a grid renders a visible gap at some counts, or when an early return on an "unsupported" size looks harmless.