feat: add blinds reveal to theme toggle - #134
Merged
starc007 merged 2 commits intoAug 1, 2026
Merged
Conversation
Adds a fourth `variant` to ThemeToggle alongside the rectangle and circle clip-path reveals. The new theme is masked in behind slats that widen shut across the viewport. mask-image is not animatable, so the slat edge is an @property-registered length that the keyframes drive; the mask re-resolves each frame it ticks. mask-size fixes the tile at 72px instead of letting a repeating gradient's last stop define it, which keeps the 20px soft edge from dragging the tile wider than the slat and leaving a feathered gap that never closes.
Owner
|
Great work on the blinds reveal — the implementation is thoughtful and the fallback behavior is well documented. One thing to address before merge:
|
Contributor
Author
|
Appreciate the review @starc007. I'll replace the inline curve with EASE_OUT_CSS and push the fix shortly. |
The blinds keyframe hardcoded cubic-bezier(0.16, 1, 0.3, 1), which is exactly EASE_OUT_CSS from lib/ease.ts. Interpolate the token so motion tuning stays centralized. The circle variants keep their Material curve, which has no token yet.
Contributor
Author
|
Added the fix for the above |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds a fourth
variantto the Theme Toggle page, alongside the existing rectangle and circle clip-path reveals.It installs under the existing
@beui/theme-toggleslug; it's a prop on the same component, not a new file.Why a mask rather than another clip-path
The three existing variants are all
clip-path— one moving boundary, hard-edged, either linear (inset) or radial (circle). A fourth shape on that same mechanic would have read as a variation, not a different reveal. Slats need many simultaneous boundaries, andclip-path: polygon()can't describe disjoint regions, so this one is a mask.That introduces the constraint the rest of the implementation is about:
mask-imageis not an animatable property. The slat edge is therefore an@property-registered<length>that the keyframes drive, and the mask expression re-resolves every frame the property ticks.Why mask-size rather than a repeating gradient
The obvious form is
repeating-linear-gradient, and the first version used one. It has a defect that only shows up at the end of the animation: the tile length of a repeating gradient is its last colour stop, so as soon as the soft edge passes 72px the tile grows with it. The opaque band can then never cover a full tile, and every slat keeps a feathered gap that never closes — a faint, permanent stripe of the old theme on the final frame before the pseudo-element is removed.Anchoring the band to the far end of the tile and growing it backwards fixes the closing end but moves the artefact to the opening frame, and it caps the feather at a few px before that becomes visible as a veil at t=0.
A plain
linear-gradientwithmask-size: 72px 100%fixes the tile independently of the stops, so both ends land clean by construction — fully transparent at-20px, fully opaque at72px— and the feather can be as wide as it wants. This is why the slats are vertical: seamless tiling undermask-sizeonly holds at 90°, since a tilted gradient doesn't wrap at the tile boundaries. The 20px soft edge was worth more than the lean.Fallback
--beui-vt-slatis unregistered on engines without@property, which makesvar()invalid at computed-value time and dropsmask-imagetonone— a fully opaque pseudo-element, so the theme swaps in one step.initial-valueis72px(closed) rather than0pxfor the same reason: an open initial value would degrade to a blank page instead of an instant reveal. Reduced motion and browsers without the View Transition API take the existingsetThemepath unchanged.Slats sweep the whole viewport and have no origin point, so
blindsignoresstart; the other three variants are untouched.Timing
700ms on
cubic-bezier(0.16, 1, 0.3, 1), which isEASE_OUTfromlib/ease.ts— matching the 700ms the circle variants already use. An earlier pass ran 640ms on a quint curve and read as a hard stop at the end.Checks
bun run check(typecheck, lint, registry) passes — 55 registry components, no new warnings.The reveal itself could not be watched end to end in an automated browser: Chrome skips view transitions on a document reporting
visibilityState: "hidden", which is what a driven tab reports, and the existingcircle-blurvariant is equally inert there. So the mask was verified by pinning--beui-vt-slatto static values on a full-viewport probe and screenshotting each:-20pxrenders fully clear,26pxrenders evenly feathered slats with no tiling seams,72pxrenders fully opaque with no residual gaps. Those three are the states the correctness argument above rests on. Worth a manual look at the live transition before merge.