UI Element Drawer with <dialog>

How to make a drawer with <dialog> using only browser APIs.

The Foundation

Here is the general HTML Structure we’re working with. Without the CSS and JS, nothing will happen.

Create a new issue

<button>Open (wont't work)</button>
<dialog>
	<button>Cancel</button>
	<h3>Create a new issue</h3>
	<form method="dialog">
		<input type="text" placeholder="Title" />
		<textarea placeholder="Write a message"></textarea>
		<button type="submit">Submit</button>
	</form>
</dialog>

Dialog Drawer JS Version

Well that stinks. But it turns out writing this same feature using the Web Animations API isn’t too rough. I’m sure this once could even be further cleaned up.

Create a new issue

<script>
	const dialog = document.querySelector('#js-dialog');
	const open = document.querySelector('#js-trigger');
	const close = document.querySelector('#js-dialog .close');
	let is_opening = false;
	let is_closing = false;

	open.addEventListener('click', toggle);
	close.addEventListener('click', toggle);

	function toggle() {
		is_opening = !dialog.open;
		is_closing = dialog.open;

		let start = `0 0`;
		let end = '0 100vb';
		let translate = is_closing ? [start, end] : [end, start];

		if (is_opening) dialog.showModal();

		window.requestAnimationFrame(() => {
			let animation = dialog.animate(
				{
					translate,
				},
				{
					duration: 300,
					easing: 'ease-in-out',
					fill: 'forwards',
				},
			);
			animation.onfinish = () => {
				if (is_closing) {
					dialog.close();
				}
				is_closing = false;
				is_opening = false;
			};
		});
	}
</script>

<button id="js-trigger">Open Dialog</button>
<dialog id="js-dialog">
	<button class="close">Cancel</button>
	<h3>Create a new issue</h3>
	<form method="dialog">
		<input type="text" placeholder="Title" />
		<textarea placeholder="Write a message"></textarea>
		<button type="submit">Submit</button>
	</form>
</dialog>

<style>
	/* UI Code */

	#js-dialog {
		translate: 0 100vb;
		height: 98vb;
		width: 100vi;
		padding: 1rem;
		border-radius: 25px 25px 0 0;
		border: none;
		max-width: 100%;
		box-shadow:
			rgba(0, 0, 0, 0.25) 0px 54px 55px,
			rgba(0, 0, 0, 0.12) 0px -12px 30px;
		inset-block-start: unset;

		&::backdrop {
			transition: opacity 0.2s 0.4s;
			background: rgba(0, 0, 0, 0.5);
		}
	}

	body:has([open]) {
		overflow: hidden;
	}

	button[type='submit'] {
		position: absolute;
		inset-inline-end: 1rem;
		inset-block-start: 1rem;
	}
</style>

Can I use this?

Yes! The only newer feature here is the Web Animations API with 97% support.

Dialog Drawer CSS Version

Choosing between the popover API and the dialog API has been a bit of a back and forth for me. Here is a version of a slide up drawer built with <dialog>

This version uses a few weakly supported APIs, so for non-experimental use cases see the JS version below.


Create a new issue

<script>
	const dialog = document.querySelector('#demo-drawer-css');
	const open = document.querySelector('#trigger');
	const close = document.querySelector('#demo-drawer-css .close');
	open.addEventListener('click', () => dialog.showModal());
	close.addEventListener('click', () => dialog.close());
</script>

<button id="trigger">Open Dialog</button>
<dialog id="demo-drawer-css">
	<button class="close">Cancel</button>
	<h3>Create a new issue</h3>
	<form method="dialog">
		<input type="text" placeholder="Title" />
		<textarea placeholder="Write a message"></textarea>
		<button type="submit">Submit</button>
	</form>
</dialog>

<style>
	#demo-drawer-css {
		transition:
			display 0.5s allow-discrete,
			overlay 0.5s allow-discrete,
			translate 0.5s,
			opacity 0.2s 0.4s;
		opacity: 0;
		translate: 0 100vh;
		height: 98vb;
		width: 100vi;
		padding: 1rem;
		border-radius: 25px 25px 0 0;
		border: none;
		max-width: 100%;
		box-shadow:
			rgba(0, 0, 0, 0.25) 0px 54px 55px,
			rgba(0, 0, 0, 0.12) 0px -12px 30px;
		inset-block-start: unset;

		&::backdrop {
			transition:
				display 0.5s allow-discrete,
				overlay 0.5s allow-discrete,
				opacity 0.2s 0.4s;
			opacity: 0;
			background: rgba(0, 0, 0, 0.3);
		}

		&[open],
		&[open]::backdrop {
			opacity: 1;
			transition:
				display 0.5s allow-discrete,
				overlay 0.5s allow-discrete,
				translate 0.5s,
				opacity 0.2s;
		}

		&[open] {
			translate: 0 0;
		}

		@starting-style {
			&[open],
			&[open]::backdrop {
				opacity: 0;
			}
			&[open] {
				translate: 0 100vh;
			}
		}
	}

	body:has([open]) {
		overflow: hidden;
	}

	/* UI Code */
	button[type='submit'] {
		position: absolute;
		inset-inline-end: 1rem;
		inset-block-start: 1rem;
	}
</style>

Can I Use this?

As of ‘2024-06-06’ you can’t really unless you are only shipping to chromium browsers (possible with an Electron app).

@starting-style (72% of users) and allow-discrete (71% of users) are the blockers.