Skip to content

Instantly share code, notes, and snippets.

@krhoyt
Last active October 3, 2025 17:32
Show Gist options
  • Select an option

  • Save krhoyt/8276d7a8350f1ba9a4d4ce6d7c07b934 to your computer and use it in GitHub Desktop.

Select an option

Save krhoyt/8276d7a8350f1ba9a4d4ce6d7c07b934 to your computer and use it in GitHub Desktop.
Three panel responsive layout with fourth panel that slides into relevant area based on viewport.
export default class HoytNavigation extends HTMLElement {
constructor() {
super();
const template = document.createElement( 'template' );
template.innerHTML = /* template */ `
<style>
:host {
box-sizing: border-box;
display: inline-block;
position: relative;
margin: 0;
padding: 0;
width: 300px;
}
section[part=menu] {
background: #2c3e50;
box-sizing: border-box;
display: flex;
flex-direction: column;
height: 100%;
margin: 0;
padding: 0;
position: relative;
z-index: 150;
}
section[part=scrim] {
background: rgba( 0, 0, 0, 0.40 );
box-sizing: border-box;
display: none;
left: 0;
height: 100%;
margin: 0;
opacity: 0;
padding: 0;
position: fixed;
top: 0;
transition: opacity 300ms ease-in-out;
width: 100%;
z-index: 125;
}
@media ( max-width: 1024px ) {
:host {
height: 100%;
left: 0;
position: fixed;
top: 0;
transition: visibility 300ms allow-discrete;
visibility: hidden;
width: 100%;
z-index: 100;
}
section[part=menu] {
box-shadow: 2px 0 5px rgba( 0, 0, 0, 0.20 );
height: 100vh;
left: -300px;
position: fixed;
top: 0;
transition: left 300ms ease-in-out;
width: 300px;
}
section[part=scrim] {
backdrop-filter: blur( 4px );
display: block;
opacity: 0;
transition: opacity 300ms ease-in-out;
}
:host( [open] ) {
visibility: visible;
}
@starting-style {
:host( [open] ) {
visibility: hidden;
}
}
:host( [open] ) section[part=scrim] {
opacity: 1.0;
}
:host( [open] ) section[part=menu] {
left: 0;
}
}
</style>
<section part="scrim">
<slot name="scrim"></slot>
</section>
<section part="menu">
<slot></slot>
</section>
`;
// Root
this.attachShadow( {mode: 'open'} );
this.shadowRoot.appendChild( template.content.cloneNode( true ) );
}
// When attributes change
_render() {;}
// Promote properties
// Values may be set before module load
_upgrade( property ) {
if( this.hasOwnProperty( property ) ) {
const value = this[property];
delete this[property];
this[property] = value;
}
}
// Setup
connectedCallback() {
this._upgrade( 'open' );
this._render();
}
// Watched attributes
static get observedAttributes() {
return [
'open'
];
}
// Observed attribute has changed
// Update render
attributeChangedCallback( name, old, value ) {
this._render();
}
// Attributes
// Reflected
// Boolean, Number, String, null
get open() {
return this.hasAttribute( 'open' );
}
set open( value ) {
if( value !== null ) {
if( typeof value === 'boolean' ) {
value = value.toString();
}
if( value === 'false' ) {
this.removeAttribute( 'open' );
} else {
this.setAttribute( 'open', '' );
}
} else {
this.removeAttribute( 'open' );
}
}
}
window.customElements.define( 'hoyt-navigation', HoytNavigation );
export default class HoytPrimary extends HTMLElement {
constructor() {
super();
const template = document.createElement( 'template' );
template.innerHTML = /* template */ `
<style>
:host {
background: #ecf0f1;
box-sizing: border-box;
display: flex;
flex-basis: 0;
flex-direction: column;
flex-grow: 1;
margin: 0;
padding: 0;
position: relative;
}
</style>
<section>
<slot></slot>
</section>
`;
// Root
this.attachShadow( {mode: 'open'} );
this.shadowRoot.appendChild( template.content.cloneNode( true ) );
}
// When attributes change
_render() {;}
// Promote properties
// Values may be set before module load
_upgrade( property ) {
if( this.hasOwnProperty( property ) ) {
const value = this[property];
delete this[property];
this[property] = value;
}
}
// Setup
connectedCallback() {
this._render();
}
// Watched attributes
static get observedAttributes() {
return [];
}
// Observed attribute has changed
// Update render
attributeChangedCallback( name, old, value ) {
this._render();
}
}
window.customElements.define( 'hoyt-primary', HoytPrimary );
export default class HoytSecondary extends HTMLElement {
constructor() {
super();
const template = document.createElement( 'template' );
template.innerHTML = /* template */ `
<style>
:host {
box-sizing: border-box;
display: flex;
flex-basis: 0;
flex-direction: column;
flex-grow: 1;
margin: 0;
overflow: hidden;
padding: 0;
position: relative;
}
section[part=placeholder] {
box-sizing: border-box;
height: 100%;
margin: 0;
padding: 0;
}
section[part=detail] {
background: #f8f9fa;
box-sizing: border-box;
height: 100%;
left: 0;
margin: 0;
padding: 0;
position: absolute;
transform: translateY( 100% );
transition: transform 300ms ease-in-out;
top: 0;
width: 100%;
}
:host( [open] ) section[part=detail] {
transform: translateY( 0 );
}
@media ( max-width: 430px ) {
:host {
bottom: 0;
height: 100%;
left: 0;
position: fixed;
transform: translateY( 100% );
transition: transform 300ms ease-in-out;
width: 100%;
z-index: 100;
}
:host( [open] ) {
transform: translateY( 0 );
}
section[part=placeholder] {
display: none;
}
section[part=detail] {
height: 100%;
position: static;
transform: none;
}
}
</style>
<section part="placeholder">
<slot name="placeholder"></slot>
</section>
<section part="detail">
<slot></slot>
</section>
`;
// Root
this.attachShadow( {mode: 'open'} );
this.shadowRoot.appendChild( template.content.cloneNode( true ) );
}
// When attributes change
_render() {;}
// Promote properties
// Values may be set before module load
_upgrade( property ) {
if( this.hasOwnProperty( property ) ) {
const value = this[property];
delete this[property];
this[property] = value;
}
}
// Setup
connectedCallback() {
this._upgrade( 'open' );
this._render();
}
// Watched attributes
static get observedAttributes() {
return [
'open'
];
}
// Observed attribute has changed
// Update render
attributeChangedCallback( name, old, value ) {
this._render();
}
// Attributes
// Reflected
// Boolean, Number, String, null
get open() {
return this.hasAttribute( 'open' );
}
set open( value ) {
if( value !== null ) {
if( typeof value === 'boolean' ) {
value = value.toString();
}
if( value === 'false' ) {
this.removeAttribute( 'open' );
} else {
this.setAttribute( 'open', '' );
}
} else {
this.removeAttribute( 'open' );
}
}
}
window.customElements.define( 'hoyt-secondary', HoytSecondary );
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="theme-color" content="#5fb2ff">
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
<meta name="description" content="Responsive three panel user interface.">
<title>Three Panel</title>
<link rel="icon" type="image/x-icon" href="three-panel.svg" />
<style>
body, html {
height: 100%;
margin: 0;
overflow: hidden;
padding: 0;
}
body {
box-sizing: border-box;
color: #161616;
display: flex;
flex-direction: row;
font-family: sans-serif;
font-weight: 400;
}
hoyt-navigation p {
color: #ffffff;
}
hoyt-navigation button[id=menu_close],
hoyt-primary button[id=menu_open] {
display: none;
}
@media ( max-width: 1024px ) {
hoyt-navigation button[id=menu_close],
hoyt-primary button[id=menu_open] {
display: inline-flex;
}
}
</style>
</head>
<body>
<hoyt-navigation>
<button id="menu_close" type="button">Close Menu</button>
<p>Navigation Menu</p>
</hoyt-navigation>
<hoyt-primary>
<button id="menu_open" type="button">Open Menu</button>
<p>Primary</p>
<button id="detail_open" type="button">Open Detail</button>
</hoyt-primary>
<hoyt-secondary>
<p slot="placeholder">Secondary</p>
<p>Detail</p>
<button id="detail_close" type="button">Close Detail</button>
</hoyt-secondary>
<script src="navigation.js" type="module"></script>
<script src="primary.js" type="module"></script>
<script src="secondary.js" type="module"></script>
<script>
const detail_close = document.querySelector( '#detail_close' );
detail_close.addEventListener( 'click', () => {
secondary.open = false;
} );
const detail_open = document.querySelector( '#detail_open' );
detail_open.addEventListener( 'click', () => {
secondary.open = true;
} );
const menu_close = document.querySelector( '#menu_close' );
menu_close.addEventListener( 'click', () => {
navigation.open = false;
} );
const menu_open = document.querySelector( '#menu_open' );
menu_open.addEventListener( 'click', () => {
navigation.open = true;
} );
const navigation = document.querySelector( 'hoyt-navigation' );
const secondary = document.querySelector( 'hoyt-secondary' );
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment