<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

External CSS

  1. https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz@0,9..40;1,9..40&amp;display=swap

External JavaScript

This Pen doesn't use any external JavaScript resources.