<div id="root"></div>
* {
border: 0;
box-sizing: border-box;
margin: 0;
padding: 0;
}
:root {
--hue: 223;
--red: hsl(3,90%,50%);
--white: hsl(0,0%,100%);
--primary: hsl(var(--hue),90%,50%);
--primary-t: hsla(var(--hue),90%,50%,0);
--gray1: hsl(var(--hue),10%,90%);
--gray2: hsl(var(--hue),10%,80%);
--gray3: hsl(var(--hue),10%,70%);
--gray4: hsl(var(--hue),10%,60%);
--gray5: hsl(var(--hue),10%,50%);
--gray6: hsl(var(--hue),10%,40%);
--gray7: hsl(var(--hue),10%,30%);
--gray8: hsl(var(--hue),10%,20%);
--gray9: hsl(var(--hue),10%,10%);
--trans-dur: 0.3s;
--trans-timing: cubic-bezier(0.65,0,0.35,1);
font-size: calc(28px + (60 - 28) * (100vw - 320px) / (3840 - 320));
}
body,
button {
color: var(--gray9);
font: 1em/1.5 "DM Sans", sans-serif;
transition:
background-color var(--trans-dur),
color var(--trans-dur);
}
body {
background-color: var(--gray1);
}
.recorder {
background-color: transparent;
cursor: pointer;
display: flex;
align-items: center;
margin: auto;
outline: transparent;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%,-50%);
-webkit-appearance: none;
appearance: none;
-webkit-tap-highlight-color: transparent;
&__label {
&-start,
&-end {
display: block;
position: relative;
}
&-start,
&-end-text {
transition: opacity var(--trans-dur);
}
&-start {
margin-inline: 0 0.5em;
}
&-end {
margin-inline: 0.5em 0;
&-text {
opacity: 0.4;
& + & {
opacity: 0;
position: absolute;
top: 0;
left: 0;
[dir="rtl"] & {
right: 0;
left: auto;
}
}
}
}
}
&__switch {
background-color: var(--white);
border-radius: 0.75em;
box-shadow:
0 0 0 0.125em var(--primary-t),
0 0.25em 0.25em hsla(0,0%,0%,0.1);
display: flex;
padding: 0.25em;
width: 2.5em;
height: 1.5em;
&,
&-handle {
transition:
background-color var(--trans-dur),
box-shadow var(--trans-dur),
transform var(--trans-dur) var(--trans-timing),
transform-origin var(--trans-dur) var(--trans-timing);
}
&-handle {
background-color: var(--gray3);
border-radius: 50%;
display: block;
transform-origin: 0 0.5em;
width: 1em;
height: 1em;
[dir="rtl"] & {
transform-origin: 100% 0.5em;
}
}
}
&__timer {
display: block;
overflow: visible;
width: 100%;
height: auto;
&-ring {
transition:
r var(--trans-dur) var(--trans-timing),
stroke-dasharray var(--trans-dur) var(--trans-timing),
stroke-dashoffset var(--trans-dur) var(--trans-timing),
stroke-width var(--trans-dur) var(--trans-timing);
}
}
&:focus-visible &__switch {
box-shadow:
0 0 0 0.125em var(--primary),
0 0.25em 0.25em hsla(0,0%,0%,0.1);
}
&:active &__switch-handle {
transform: scaleX(1.5);
}
// recording state
&[aria-pressed="true"] &__label-start {
opacity: 0.4;
}
&[aria-pressed="true"] &__label-end-text {
opacity: 0;
}
&[aria-pressed="true"] &__label-end-text + &__label-end-text {
opacity: 1;
}
&[aria-pressed="true"] &__switch-handle {
background-color: var(--red);
transform: translateX(100%);
transform-origin: 100% 0.5em;
[dir="rtl"] & {
transform: translateX(-100%);
transform-origin: 0 0.5em;
}
}
&[aria-pressed="true"] &__timer-ring {
r: 6.5px;
stroke-width: 3px;
}
&[aria-pressed="true"]:active &__switch-handle {
transform: translateX(100%) scaleX(1.5);
[dir="rtl"] & {
transform: translateX(-100%) scaleX(1.5);
}
}
}
/* Dark theme */
@media (prefers-color-scheme: dark) {
body,
button {
color: var(--gray1);
}
body {
background-color: var(--gray9);
}
.recorder {
&__switch {
background-color: var(--gray8);
box-shadow:
0 0 0 0.125em var(--primary-t),
0 0.25em 0.25em hsla(0,0%,0%,0.2);
&-handle {
background-color: var(--gray6);
}
}
&:focus-visible &__switch {
box-shadow:
0 0 0 0.125em var(--primary),
0 0.25em 0.25em hsla(0,0%,0%,0.2);
}
}
}
View Compiled
import React, { StrictMode, useEffect, useState } from "https://esm.sh/react";
import { createRoot } from "https://esm.sh/react-dom/client";
createRoot(document.getElementById("root")!).render(
<StrictMode>
<RecordingToggle />
</StrictMode>
);
function RecordingToggle() {
const [recording, setRecording] = useState(false);
const [time, setTime] = useState(0);
const timeMax = 60;
const [timeStopped, setTimeStopped] = useState(0);
const circumference = recording ? 40.84 : 50.27;
const circumferencePart = recording ? 1 - (time / timeMax) : 1;
const strokeDashArray = `${circumference} ${circumference}`;
const strokeDashOffset = +(circumference * circumferencePart).toFixed(2);
function timeFormatted() {
const timeToDisplay = recording ? time : timeStopped;
const minutes = `0${Math.floor(timeToDisplay / 60)}`.slice(-2);
const seconds = `0${timeToDisplay % 60}`.slice(-2);
return `${minutes}:${seconds}`;
}
// timer loop
useEffect(() => {
let frameId = 0;
if (recording) {
setTimeStopped(0);
const render = () => {
setTime((time) => time + 1);
// allow the time to be shown in the transition when stopping
setTimeStopped((time) => time + 1);
frameId = setTimeout(render,1e3);
};
frameId = setTimeout(render,1e3);
} else {
setTime(0);
clearTimeout(frameId);
}
return () => {
clearTimeout(frameId);
};
}, [recording]);
// stop automatically if time hits limit
useEffect(() => {
if (time >= timeMax) {
setRecording(false);
}
}, [time])
return (
<button
className="recorder"
type="button"
aria-pressed={recording}
onClick={() => setRecording(!recording)}
>
<span className="recorder__label-start" aria-hidden={recording}>Stop</span>
<span className="recorder__switch">
<span className="recorder__switch-handle">
<svg className="recorder__timer" viewBox="0 0 16 16" width="16px" height="16px" aria-hidden="true">
<g fill="none" strokeLinecap="round" strokeWidth="0" transform="rotate(-90,8,8)">
<circle className="recorder__timer-ring" stroke="hsla(0,0%,100%,0.3)" cx="8" cy="8" r="8" />
<circle className="recorder__timer-ring" stroke="hsla(0,0%,100%,0.5)" cx="8" cy="8" r="8" strokeDasharray={strokeDashArray} strokeDashoffset={strokeDashOffset} />
</g>
</svg>
</span>
</span>
<span className="recorder__label-end" aria-hidden={!recording}>
<span className="recorder__label-end-text">Record</span>
<span className="recorder__label-end-text">{timeFormatted()}</span>
</span>
</button>
)
}
View Compiled
This Pen doesn't use any external JavaScript resources.