You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The intent is good — in dark mode a drop shadow is black on near-black and gives an elevated surface no edge, so the ring supplies one. But the mechanism is fragile in a way that isn't visible from the theme file.
The problem
An inset shadow paints above the element's own background but below its children. So any opaque child laid flush to the inner edge erases the ring for exactly as far as it extends. Section — the standard content wrapper, opaque --color-background-surface in its default variant — does precisely that.
The failure mode is worse than "no ring": it's a partial ring. Content covers the top of the surface, empty space below it does not, so the ring switches on partway down and reads to a viewer as the border changing width. Measured on the docs site, BottomSheet's left edge in dark mode (neutral theme):
outside
edge
just inside
above the content end
2,2,2
60,60,60
38,38,38
below the content end
2,2,2
60,60,60
71,71,71
This is what a user reported as "the bottom sheet's border width is not consistent."
Why a component can't fix it properly
The ring is inside a shadow token, so from a component there is no way to read it separately, lift it above children, or opt a child out. The only local moves are: hide it evenly, leave it half-covered, or make content wrappers transparent (which breaks Section's contract). PR #5305 takes the first — the BottomSheet's scrolling body paints the surface across the whole inner box, hiding the ring, and the sheet draws its own --color-border hairline instead. That fixes the symptom for one component and leaves the sheet's edge treatment diverging from the theme's other elevated surfaces.
Scope
Any elevated surface whose direct content can be opaque and flush to its edge — Dialog (same --shadow-high, and its inner wrapper sets no background), Popover, HoverCard, DropdownMenu, Card with a full-bleed opaque child. Confirmed on BottomSheet; I did not measure the others (the Dialog docs example is fullscreen, and a top-layer dialog didn't pick up the themed shadow in my local harness, so that one needs checking in a real app).
Card is the interesting counter-example: it composes --_card-elevation and --_card-ring into its box-shadow list and draws a real borderWidth/borderColor. A component that owns a border is immune, because a border paints outside the padding box where no child can reach.
Options worth discussing
A dedicated --surface-ring token that components apply as a border (or outline), not a shadow. Immune to children by construction; themes keep control of colour and width. Costs a token and a pass over the elevated surfaces.
Themes stop putting rings in shadows, and dark-mode edge definition comes from --color-border on each surface. Simplest model; means the elevated surfaces need a border they mostly don't have today.
Status quo plus a documented rule: "an elevated surface must not let an opaque child reach its inner edge," enforced by each surface painting its own background across its inner box (what fix(BottomSheet): give the sheet one uniform edge against the scrim #5305 does). Cheapest, but it's a rule nobody will remember, and it silently defeats the theme's ring.
I lean 1 — it keeps the theme's intent, works with an arbitrary child, and matches what Card already does by hand.
Related: #5305 (the BottomSheet fix that motivated this), #5014 (overriding one side of a colour pair voids the contrast guarantee), #2150 (dark mode research/guidelines).
Every bundled theme adds a hairline inner ring to the elevation shadow tokens, dark mode only. Neutral's:
The intent is good — in dark mode a drop shadow is black on near-black and gives an elevated surface no edge, so the ring supplies one. But the mechanism is fragile in a way that isn't visible from the theme file.
The problem
An inset shadow paints above the element's own background but below its children. So any opaque child laid flush to the inner edge erases the ring for exactly as far as it extends.
Section— the standard content wrapper, opaque--color-background-surfacein its default variant — does precisely that.The failure mode is worse than "no ring": it's a partial ring. Content covers the top of the surface, empty space below it does not, so the ring switches on partway down and reads to a viewer as the border changing width. Measured on the docs site, BottomSheet's left edge in dark mode (neutral theme):
2,2,260,60,6038,38,382,2,260,60,6071,71,71This is what a user reported as "the bottom sheet's border width is not consistent."
Why a component can't fix it properly
The ring is inside a shadow token, so from a component there is no way to read it separately, lift it above children, or opt a child out. The only local moves are: hide it evenly, leave it half-covered, or make content wrappers transparent (which breaks Section's contract). PR #5305 takes the first — the BottomSheet's scrolling body paints the surface across the whole inner box, hiding the ring, and the sheet draws its own
--color-borderhairline instead. That fixes the symptom for one component and leaves the sheet's edge treatment diverging from the theme's other elevated surfaces.Scope
Any elevated surface whose direct content can be opaque and flush to its edge —
Dialog(same--shadow-high, and its inner wrapper sets no background),Popover,HoverCard,DropdownMenu,Cardwith a full-bleed opaque child. Confirmed on BottomSheet; I did not measure the others (the Dialog docs example is fullscreen, and a top-layer dialog didn't pick up the themed shadow in my local harness, so that one needs checking in a real app).Cardis the interesting counter-example: it composes--_card-elevationand--_card-ringinto its box-shadow list and draws a realborderWidth/borderColor. A component that owns a border is immune, because a border paints outside the padding box where no child can reach.Options worth discussing
--surface-ringtoken that components apply as a border (or outline), not a shadow. Immune to children by construction; themes keep control of colour and width. Costs a token and a pass over the elevated surfaces.--color-borderon each surface. Simplest model; means the elevated surfaces need a border they mostly don't have today.I lean 1 — it keeps the theme's intent, works with an arbitrary child, and matches what Card already does by hand.
Related: #5305 (the BottomSheet fix that motivated this), #5014 (overriding one side of a colour pair voids the contrast guarantee), #2150 (dark mode research/guidelines).