Pen Settings

HTML

CSS

CSS Base

Vendor Prefixing

Add External Stylesheets/Pens

Any URL's added here will be added as <link>s in order, and before the CSS in the editor. If you link to another Pen, it will include the CSS from that Pen. If the preprocessor matches, it will attempt to combine them before processing.

+ add another resource

JavaScript

Babel includes JSX processing.

Add External Scripts/Pens

Any URL's added here will be added as <script>s in order, and run before the JavaScript in the editor. You can use the URL of any other Pen and it will include the JavaScript from that Pen.

+ add another resource

Packages

Add Packages

Search for and use JavaScript packages from npm here. By selecting a package, an import statement will be added to the top of the JavaScript editor for this package.

Behavior

Save Automatically?

If active, Pens will autosave every 30 seconds after being saved once.

Auto-Updating Preview

If enabled, the preview panel updates automatically as you code. If disabled, use the "Run" button to update.

Format on Save

If enabled, your code will be formatted when you actively save your Pen. Note: your code becomes un-folded during formatting.

Editor Settings

Code Indentation

Want to change your Syntax Highlighting theme, Fonts and more?

Visit your global Editor Settings.

HTML

              
                .h-screen.p-16.bg-gray-200.antialiased.flex.flex-col.justify-center
	.shadow-lg.rounded-lg.max-w-2xl.bg-gray-400.mx-auto
		main.px-8.py-6
			h1.font-bold.text-2xl.leading-tight.text-gray-900 CSS Aspect Ratio, Rendering Engines, and More with Jen Simmons
			p.mt-8 Jen Simmons is back on the show to talk with us about her new HTML Essentials course, CSS Aspect Ratio, rendering engines vs browsers, and a big announcement from her personally! 
		
		//- THE ACTUAL AUDIO PLAYER STARTS HERE
		#player.mt-12.bg-gray-800.text-gray-300.px-8.py-6.rounded-b-lg
		
			//- This is where I hide the <audio> element
			audio(src="https://chtbl.com/track/643D/cdn.simplecast.com/audio/167887/167887a0-ac00-4cf9-bc69-b5ca845997db/115a8d3c-5349-40f2-94f3-6bd45d9356a1/shoptalkshow-415_tc.mp3")
			.flex.gap-8.items-center
				img#artwork.-mt-12.border-2.border-gray-300.rounded-md(width="120" height="120" src="https://i2.wp.com/shoptalkshow.com/wp-content/uploads/2019/10/Jen-Simmons-2019.jpg?resize=80%2C80&ssl=1", alt="")
				
				#timeline.flex.w-full.gap-4.items-center
					//- The SVG icons inside this <button> are shown/hidden based on data-state
					button#playToggle.border.border-gray-500.text-white.rounded-full.px-4.py-2(aria-label="Play" data-state='paused')
						span.play
							svg.fill-current(viewbox='0 0 20 20' width='20' height='20')
								polygon(points="4 4 16 10 4 16")
						span.pause
							svg.fill-current(viewbox='0 0 20 20' width='20' height='20')
								path(d='M5,4 L8,4 L8,16 L5,16 L5,4 Z M12,4 L15,4 L15,16 L12,16 L12,4 Z')

					//- This timer updates automatically based on audio element progress events
					span.sr-only Current time:
					span.w-12.text-right.text-sm#currentTime 0:00:00

					//- These are two elements placed on top of each other:
					//- + <input type="range"> is used to let the user seek through the audio
					//- + <progress> is used to show the progress of the audio
					//-
					//- Both elements' values are updated based on audio progress events
					.flex-grow#playback-bar
						progress#progress.w-full.block.h-1.appearance-none.border-0.mt-2(value='0' max='1' step='0.01')
						input#scrubber.w-full.-mt-3.block.z-10.relative.appearance-none.bg-transparent(type='range' value='0' max='1' step='0.01')

					//- This time is set when the audio is loaded and the duration is known
					span.sr-only Total time:
					span.w-12.text-right.text-sm#durationTime 0:00:00

              
            
!

CSS

              
                #playToggle[data-state='playing'] .play
	display none
	
#playToggle[data-state='paused'] .pause
	display none
	
:root {
	--gray-600: #718096
	--gray-400: #CBD5E0
	--gray-200 #EDF2F7
	
	--purple-500 #9F7AEA
	
	--green-500: #48BB78
	
	--blue-500 #4299E1
}
	
progress
	background-color var(--gray-600)

progress::-moz-progress-bar
	background-color var(--gray-200)
	
#playback-bar:hover progress::-moz-progress-bar
	background-color var(--blue-500)
	
input[type=range]::-moz-range-thumb,
input[type=range]::-webkit-slider-thumb
	appearance none
	background var(gray-200)
	border none
	opacity 0
	width 5px
	
#playback-bar:hover
	input[type=range]::-moz-range-thumb,
	input[type=range]::-webkit-slider-thumb
		opacity 1
		
// *
// 	outline: 1px solid red

#currentTime,
#durationTime
	font-feature-settings 'tnum'
              
            
!

JS

              
                console.clear()
const { createMachine, interpret, assign } = XState

const playToggle = document.querySelector('#playToggle') // <button>
const audio = document.querySelector('audio') // <audio>
const currentTime = document.querySelector('#currentTime') // <time>
const durationTime = document.querySelector('#durationTime') // <time>
const scrubber = document.querySelector('#scrubber') // <input type=range>
const progress = document.querySelector('#progress') // <progress>

const machine = createMachine({
	initial: 'paused',
	context: {
		currentTime: 0, // (s)
		durationTime: 0, // (s)
	},
	on: {
		durationchange: {
			actions: [
				assign({
					durationTime: (context, event) => event.target.duration
				}),
				'renderDurationTime'
			]
		},
		timeupdate: {
			actions: [
				assign({
					currentTime: (context, event) => event.target.currentTime
				}),
				'renderCurrentTime',
				'updateScrubber'
			]
		},
		SCRUB: {
			actions: [
				assign({
					currentTime: (context, event) => event.value * context.durationTime
				}),
				'updateAudioCurrentTime',
				'renderCurrentTime',
				'updateScrubber'
			]
		},
		seeked: {
			actions: [
				assign({
					currentTime: (context, event) => event.target.currentTime
				}),
				'renderCurrentTime',
				'updateScrubber'
			]
		}
	},
	states: {
		paused: {
			on: {
				TOGGLE_PLAY: {
					target: 'playing',
					actions: ['playAudio']
				},
			}
		},
		playing: {
			on: {
				TOGGLE_PLAY: {
					target: 'paused',
					actions: ['pauseAudio']
				},
			}
		}
	}
}, {
	actions: {
		playAudio: () => {
			audio.play()
			playToggle.dataset.state = 'playing'
		},
		pauseAudio: () => {
			audio.pause()
			playToggle.dataset.state = 'paused'
		},
		updateAudioCurrentTime: (context, event) => {
			audio.currentTime = context.currentTime
		},
		renderCurrentTime: (context) => {
			currentTime.innerText = formatDuration(context.currentTime)
		},
		renderDurationTime: (context) => {
			durationTime.innerText = formatDuration(context.durationTime)
		},
		updateScrubber: (context) => {
			// update position of the <input type="range"> element
			scrubber.value = context.currentTime / context.durationTime
			// update position of the <progress> element
			progress.value = context.currentTime / context.durationTime
		}
	}
})

function formatDuration(duration) {
	function formatTime(int, options) {
		const leadingZero = options && options.leadingZero
		return Math.floor(int).toString().padStart(leadingZero ? 2 : 0, '0')
	}
	
	const hours = Math.floor(duration / 60 / 60)
	const minutes = (duration - (hours * 60 * 60)) / 60
	const seconds = duration % 60
	
	return [
		formatTime(hours),
		formatTime(minutes, { leadingZero: true }),
		formatTime(seconds, { leadingZero: true })
	].join(':')
}

const service = interpret(machine).start()

playToggle.addEventListener('click', event => {
	service.send({ type: 'TOGGLE_PLAY' })
})

audio.addEventListener('timeupdate', service.send)
audio.addEventListener('durationchange', service.send)
// audio.addEventListener('seeked', service.send)

scrubber.addEventListener('input', event => {
	service.send({
		type: 'SCRUB',
		value: parseFloat(event.target.value)
	})
})

              
            
!
999px

Console