<header class="w-screen bg-orange-900 h-64"></header>
<main id="app" class="wrapper">
	<aside class="sticky top-0 ml-8 pt-16">
			<transition-group name="list" tag="ul" ref="links">
				<li v-for="(item, index) in activeHeadings" v-bind:key="item.id">
						<a @click="(e) => handleLinkClick(e, item.id)" :href="item.id" v-bind:class="{ 
							'is-active': item.id === visibleId,
							'is-child': item.depth === 2}">
							{{ item.text }}
						</a>
				</li>
			</transition-group>
	</aside>
	<div class="max-w-2xl content mt-32" v-html="compiledMarkdown" ref="content"></div>
</main>
<footer class="w-screen bg-orange-900 h-64 mt-16"></footer>
html {
	height: 100%;
}


body {
	display: flex;
	flex-direction: column;
	margin: auto;
  background-color: white;
	font-family: 'Cabin', sans-serif;
  font-weight: 400;
  line-height: 1.65;
  color: #333;
	font-size: 20px;
	
	&::before {
		content: "";
		position: fixed;
		top: 0; 
		left: 0;
		width: 100%; 
		height: 100%;  
		opacity: .05; 
		z-index: -1;
		background: url("https://static.thenounproject.com/png/2590971-200.png");
		background-size: 200px 200px;
		background-repeat: repeat;
		background-position: center center;
	}
}


h1, h2, h3, h4, h5 {
	font-family: 'Oswald', sans-serif;
  margin: 2.75rem 0 1.05rem;
  font-weight: 400;
  line-height: 1.15;
}

h1 {
  font-size: 3.052em;
}

h2 {font-size: 2.441em;}

h3 {font-size: 1.953em;}

h4 {font-size: 1.563em;}

h5 {font-size: 1.25em;}

small, .text_small {font-size: 0.8em;}

header {
	background-image: url("https://source.unsplash.com/N09f4fB92hI/1800x900");
	background-size: cover;
	background-position: top center;
}

footer {
	background-image: url("https://source.unsplash.com/xobfytNlC8Y/1800x900");
	background-size: cover;
	background-position: bottom center;
}

.sticky {
  position: sticky;
	top: 0;
}

.wrapper {
	margin: 0 auto;
	display: grid;
  padding: 10px;
  grid-template-columns: 300px auto;
  grid-gap: 30px;
	align-items: flex-start;
}

.is-active {
	font-weight: 700;
	color: #bb2205;
}

.is-child {
	padding-left: 1em;
}


.list-leave-active, .list-move {
  transition: 
		transform 0.8s, 
		opacity 0.4s;
}

.list-enter-active { 
	transition: 
		transform 0.8s ease 0.4s, 
		opacity 0.4s ease 0.4s;
}

.list-enter, .list-leave-to {
  opacity: 0;
  transform: translateY(10px);
}

.list-leave-active {
  position: absolute;
}
View Compiled
const App = {
	el: '#app',
	data() { return {
		input: '# Loading...',
		visibleId: '',
		previousVisibleId: '',
		mdLength: 0,
		headings: [],
	}}, 
	async created() {
		let url = 'https://gist.githubusercontent.com/lisilinhart/e9dcf5298adff7c2c2a4da9ce2a3db3f/raw/2f1a0d47eba64756c22460b5d2919d45d8118d42/red_panda.md'
		let response = await fetch(url)
		let data = await response.text()
		this.input = data
	},
	mounted() {
		let options = {
			rootMargin: "0px 0px -200px 0px",
      threshold: 1
		}
		const debouncedFunction = this.debounce(this.handleObserver)
		this.observer = new IntersectionObserver(debouncedFunction,options)
		this.motionQuery = window.matchMedia('(prefers-reduced-motion)')
	},
	methods: {
		findHeadings() {
			if(this.observer) {
				this.headings = [...this.$refs.content.querySelectorAll('[id]')]
				this.headings.map(heading => this.observer.observe(heading))
			}
  	},
		slugify(text) {
			return text.toString()
				.normalize('NFD')
				.replace(/[\u0300-\u036f]/g, '')
				.toLowerCase()
				.trim()
				.replace(/\s+/g, '-')
				.replace(/[^\w-]+/g, '')
				.replace(/--+/g, '-')
		},
		getRelated(item) {
			if(item) {
				const items = this.compiledHeadings
				const currentIdx =  items.indexOf(item)
				let idx = 0

				// find the correct (parent) index
				if(item.depth === 1) {
					idx = currentIdx + 1
				} else {
					// find parent index
					let found = false
					for (let j = currentIdx; j >= 0; j--) {
						if(items[j].depth === 1 && !found) { 
							idx = j + 1
							found = true
						} 
					}
				}

				let children = []
				let isSameLevel = true
				for (idx; idx < items.length; idx++) {
					if(items[idx].depth === 2 && isSameLevel) { 
						children.push(items[idx]) 
					} 
					else if(items[idx].depth === 1) { isSameLevel = false }
				}
				return children
			}
		},
		handleObserver(entries, observer) {
			entries.forEach((entry)=> {
				const { target, isIntersecting, intersectionRatio } = entry
				if (isIntersecting && intersectionRatio >= 1) {
					this.visibleId = `#${target.getAttribute('id')}`
				}
			})
		},
		 handleLinkClick(evt, itemId) {
      evt.preventDefault()
			let id = itemId.replace('#', '')
      let section = this.headings.find(heading => heading.getAttribute('id') === id)
      section.setAttribute('tabindex', -1)
      section.focus()
			this.visibleId = itemId

      window.scroll({
        behavior: this.motionQuery.matches ? 'instant' : 'smooth',
        top: section.offsetTop -20,
        block: 'start'
      })
    },
		debounce(fn) {
			var timeout;
			return function () {
				var context = this;
				var args = arguments;
				if (timeout) {
					window.cancelAnimationFrame(timeout);
				}
				timeout = window.requestAnimationFrame(function () {
					fn.apply(context, args);
				});
			}
		}
	},
	watch: {
		mdLength: function (val) {
      this.findHeadings()
    },
	},
	computed: {
    compiledMarkdown () {
			let htmlFromMarkdown = marked(this.input, { sanitize: true });
			this.mdLength = htmlFromMarkdown.length
      return htmlFromMarkdown
    },
		compiledHeadings() {
			let regexString = /#(.*)/g
			const found = this.input.match(regexString);
			const headings = found.map(item => {
				let depth = (item.match(/#/g) || []).length
				let text = item.replace(/#/gi, '','').trim()
				return {
					depth,
					id: `#${this.slugify(text)}`,
					text
				}
			})
			return headings
		},
		activeHeadings() {
			let activeItem = this.compiledHeadings.find(item => item.id === this.visibleId)
			let relatedItems = this.getRelated(activeItem) || []
			return this.compiledHeadings.filter(item => item.depth === 1 || relatedItems.includes(item))
		}
  },
};

new Vue(App)
View Compiled

External CSS

  1. https://cdnjs.cloudflare.com/ajax/libs/tailwindcss/1.9.1/tailwind.min.css
  2. https://fonts.googleapis.com/css?family=Cabin:400,500,600,700
  3. https://fonts.googleapis.com/css?family=Oswald:200,300,400,500,600,700

External JavaScript

  1. https://cdnjs.cloudflare.com/ajax/libs/marked/1.1.1/marked.min.js
  2. https://cdnjs.cloudflare.com/ajax/libs/vue/2.6.11/vue.min.js