4.6 KiB
Cooldown Button Component
A square Blazor button with a 12-second cooldown animation: when clicked, the button greys out and a circular transparency wedge dials open clockwise, progressively revealing the normal button state underneath.
Visual Design
The button has two visual states:
Idle state – A solid square button with the project's standard --paper background and --primary border. Hover inverts or brightens the paper. Click triggers a brief scale-down.
Cooldown state – The native button fades to opacity: 0 while an absolutely-positioned overlay covers it. The overlay uses a conic-gradient CSS mask to create a "dialling open" effect. A number in the centre shows the remaining seconds.
Core Technique: Conic-Gradient Mask
The cooldown reveal is accomplished with a conic-gradient mask applied to the overlay <div>:
mask-image: conic-gradient(
transparent 0deg,
transparent {angle}deg,
#000 {angle}deg,
#000 360deg
);
transparent– lets the button underneath show through (revealed area)#000(black) – fully masks the overlay, making it visible (greyed-out area)
At angle = 0deg, transparent covers nothing and the mask is entirely black → the overlay is fully opaque (button completely greyed out).
At angle = 360deg, transparent covers the full circle and black covers nothing → the overlay is fully transparent (button completely visible).
The angle animates linearly from 0 to 360 over the cooldown duration. Because conic-gradient starts at the 12 o'clock position and sweeps clockwise, the reveal begins at the top of the button and rotates around, like a clock hand or a dial opening.
Implementation Architecture
Component Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
ChildContent |
RenderFragment |
null |
Text or content inside the button |
OnClick |
EventCallback |
— | Fired when the button is clicked |
CooldownSeconds |
int |
12 |
Duration of the cooldown in seconds |
Size |
int |
120 |
Width and height in pixels (square) |
Timer Loop
A System.Timers.Timer fires every ~33 ms (≈30 fps) during the cooldown:
OnTick:
elapsed = UtcNow - startTime
if elapsed >= CooldownSeconds → end cooldown, dispose timer
_elapsedAngle = (elapsed / CooldownSeconds) * 360
_remainingSeconds = CooldownSeconds - (int)elapsed
InvokeAsync(StateHasChanged)
On each tick, _elapsedAngle is written into the overlay's inline style attribute, causing Blazor to re-render the mask-image. The timer is disposed in Dispose() to prevent leaks.
Disposal
The component implements IDisposable to clean up the timer when the component is removed from the render tree. This follows the same pattern used by SearchDialogComponent and BuildChartComponent elsewhere in the codebase.
CSS Masking Details
Two vendor-prefixed properties are set to ensure cross-browser support:
mask-image: conic-gradient(...);
-webkit-mask-image: conic-gradient(...);
The overlay uses pointer-events: none and user-select: none so that interaction passes through to the button underneath (which is disabled and transparent).
An rgba(22, 22, 24, 0.82) semi-transparent background on the overlay produces the greyed-out appearance. The mask controls where this background is visible.
Usage on the Home Page
The component is added to Pages/Pages/Home/HomePage.razor inside the first PaperComponent:
<CooldownButtonComponent CooldownSeconds="12"
Size="120"
OnClick="OnCooldownClick">
Click Me
</CooldownButtonComponent>
The OnCooldownClick handler in the page's @code block currently returns Task.CompletedTask (a no-op). This is the extension point where real work (e.g. triggering a game action, calling an API, showing a toast) would go.
Adapting the Component
- Change cooldown duration – set
CooldownSecondsto any positive integer. - Change button size – set
Sizeto any pixel dimension (button remains square). - Custom content – pass any Blazor markup as
ChildContent(text, icons, spinners). - Handle the click – attach a handler to
OnClickthat returnsTaskorvoid.
The --cooldown-size CSS custom property is set inline on the wrapper so that the label, overlay, and button all scale together.