Skip to content

Instantly share code, notes, and snippets.

@hiranthi
Last active April 18, 2025 06:19
Show Gist options
  • Save hiranthi/f549a634352db31ee0da43025c58a5bb to your computer and use it in GitHub Desktop.
Save hiranthi/f549a634352db31ee0da43025c58a5bb to your computer and use it in GitHub Desktop.
Add a scrollable ToC (incl. "back to top"-link) to the Laravel Docs
# Description
For some reason the Laravel Docs don't have a scrollable Table of Contents. Nor a "back to top"-link. Which I found very annoying.. Luckily that was rather easily fixed with a bit of custom CSS/JS "magic" ;-)
# What you need:
1. A browser extension for adding custom CSS and JS to websites of your choice.
a. User Javascript and CSS (Chrome): https://chromewebstore.google.com/detail/user-javascript-and-css/nbhcbdghjpllgmfilhnhkllmkecfmpld
b. Firemonkey (Firefox): https://addons.mozilla.org/nl/firefox/addon/firemonkey/
2. Settings (based on the Chrome extension, which I use):
a. URL pattern: `https://laravel.com/docs/*`
b. Active modules: jQuery 3 (I don't know about Firemonkey, but in the Chrome-extension this was easily activated with a checkbox)
c. JavaScript settings: Run at the start (which may or may not be active by default)
d. The rule itself has to be activated, obviously ;-)
// Defaults
:root {
--ci-primary-color: var(--color-laravel-red, #f61500);
--ci-bg-color: var(--docsearch-footer-background);
--ci-content-before-item: "# ";
--ci-content-before-item-active: '❯ ';
}
// Light theme
html[data-theme="light"] {
--ci-box-shadow: rgba(187,187,187,1);
--ci-header-color: var(--color-sand-light-1);
--ci-link-color: var(--color-sand-light-12);
--ci-icon-color: var(--ci-header-color);
--ci-icon-bg-hover-color: var(--docsearch-hit-color, #333);
--ci-active-border-color: rgba(150,159,175,0.25);
}
// Dark theme
html[data-theme="dark"] {
--ci-box-shadow: var(--color-sand-dark-3);
--ci-header-color: var(--color-sand-light-1);
--ci-link-color: var(--color-sand-dark-11);
--ci-icon-color: var(--ci-header-color);
--ci-icon-bg-hover-color: var(--docsearch-hit-color);
--ci-active-border-color: rgba(150,159,175,0.25);
}
// The actual CSS
html {
scroll-behavior: smooth;
}
#main-content h1 + ul li,
#main-content h1 + ul ul {
padding: 0;
margin: 0;
}
#main-content h1 + ul a {
padding: .5rem;
display: block;
}
#main-content h1 + ul a:hover {
text-decoration: underline;
}
#main-content h1 + ul ul li {
margin-left: 1rem;
}
body.hasCustomIndex {
position: relative;
}
body.hasCustomIndex #customDocIndex {
position: fixed;
right: 1rem;
bottom: 1rem;
display: block;
background: var(--ci-bg-color);
z-index: 2;
font-size: .9rem;
min-width: 20rem;
-webkit-box-shadow: 0px 0px 5px 0px var(--ci-box-shadow);
-moz-box-shadow: 0px 0px 5px 0px var(--ci-box-shadow);
box-shadow: 0px 0px 5px 0px var(--ci-box-shadow);
}
body.hasCustomIndex #customDocIndex * {
transition: all .2s ease;
-moz-transition: all .2s ease;
-webkit-transition: all .2s ease-in-out;
}
body.hasCustomIndex #customDocIndex h1 {
font-weight: bold;
color: var(--ci-header-color);
background-color: var(--ci-primary-color);
padding: .5rem 1rem;
font-size: 1rem;
position: relative;
}
body.hasCustomIndex #customDocIndex h1 span {
position: absolute;
right: 0;
top: 0;
display: block;
width: auto;
height: 100%;
font-weight: 900;
}
body.hasCustomIndex #customDocIndex h1 .back-to-top,
body.hasCustomIndex #collapseCustomDocIndex {
display: inline-block;
padding: .5rem 1rem;
background-color: var(--ci-primary-color);
color: var(--ci-icon-color);
}
body.hasCustomIndex #collapseCustomDocIndex {
transform: rotate(90deg);
height: 100%;
width: auto;
}
body.hasCustomIndex #collapseCustomDocIndex::before {
display: block;
height: 2rem;
}
body.hasCustomIndex #customDocIndex h1 .back-to-top {
font-weight: 900;
}
body.hasCustomIndex #customDocIndex h1 .back-to-top:hover,
body.hasCustomIndex #collapseCustomDocIndex:hover {
background-color: var(--ci-icon-bg-hover-color);
text-decoration: none;
cursor: pointer;
}
body.hasCustomIndex #collapseCustomDocIndex.collapsed::before {
content: '❯';
}
body.hasCustomIndex #collapseCustomDocIndex.folded::before {
content: '❮';
}
body.hasCustomIndex #customDocIndexList {
display: block;
overflow-y: auto;
max-height: calc(100vh - 4.75rem);
}
body.hasCustomIndex #customDocIndexList > ul {
padding: 1rem 1rem 3rem 1rem;
}
body.hasCustomIndex #customDocIndex li:before {
content: var(--ci-content-before-item);
color: var(--ci-primary-color);
position: absolute;
width: .5rem;
height: 1.8em;
vertical-align: middle;
}
body.hasCustomIndex #customDocIndex li.active:before {
content: var(--ci-content-before-item-active);
}
body.hasCustomIndex #customDocIndex li {
font-weight: 500;
line-height: 2em;
position: relative;
}
body.hasCustomIndex #customDocIndex li ul {
margin-left: 1rem;
}
body.hasCustomIndex #customDocIndex .subheaders li {
font-weight: normal;
font-size: 90%;
}
body.hasCustomIndex #customDocIndex > ul > li {
border-right: .2rem solid var(--custom-index-background);
}
body.hasCustomIndex #customDocIndex > ul > li.active {
border-color: var(--ci-active-border-color);
}
body.hasCustomIndex #customDocIndex li.active > a {
text-decoration: underline;
}
body.hasCustomIndex #customDocIndex ul:not(.subheaders) li.active > a {
font-weight: 900;
}
body.hasCustomIndex #customDocIndex li.active li.active > a {
font-weight: inherit;
}
body.hasCustomIndex #customDocIndex li.active > a:hover {
text-decoration: none;
}
body.hasCustomIndex #customDocIndex li a {
display: block;
padding-left: 1rem;
transition: all 100ms ease-in-out;
color: var(--ci-link-color);
}
body.hasCustomIndex #customDocIndex a:hover {
text-decoration: underline;
}
body.hasCustomIndex #customBackToTop {
position: relative;
display: block;
}
body.hasCustomIndex #customBackToTop a {
position: absolute;
display: block;
right: 0;
bottom: 0;
padding: .5rem 1rem;
background-color: var(--ci-primary-color);
color: var(--ci-icon-color);
font-weight: 900;
font-size: 1rem;
}
body.hasCustomIndex #customBackToTop a:hover {
background-color: var(--ci-icon-bg-hover-color);
text-decoration: none;
}
/* Carbon Ads at the bottom (hiding it to prevent overlap issues with the "back to top" / ToC elements) */
#carbonads {
z-index: 0;
right: -100rem;
bottom: -100rem;
}
jQuery( document ).ready(function($) {
$('#on-this-page-content').parent().parent().parent().remove();
if ($('#main-content h1 + ul').length > 0) {
const docIndex = $('#main-content h1 + ul');
$('body').addClass('hasCustomIndex');
$('body').append('<aside id="customDocIndex" class="shadow-lg"><h1>Table of Contents <span><button id="collapseCustomDocIndex" class="collapsed" title="Toggle Index" aria-collapsed="true"></button><a href="#main-content" class="back-to-top" style="display:none" title="Back to Top">&uarr;</a></span></h1><div id="customDocIndexList"><ul></ul></div></aside>');
$('#main-content h2').each(function (){
const articleSection = $(this).nextUntil('h2').addBack();
const sectionId = $(this).attr('id');
articleSection.wrapAll( '<section class="article-section" id="section-'+sectionId+'"></section>' );
});
// Walk through the sections
$('section.article-section').each(function(){
const sectionHeader = $(this).find('h2');
const sectionHeaderId = $(sectionHeader).attr('id');
$('<li id="sectionLink-'+sectionHeaderId+'"><a href="#section-'+sectionHeaderId+'">'+$(sectionHeader).text()+'</a></li>').appendTo('#customDocIndexList > ul');
// Walk through the subsections (if they exist)
const sectionSubheaders = $(this).find('h3');
if (sectionSubheaders.length) {
$('<ul class="subheaders"></ul>').appendTo('li#sectionLink-'+sectionHeaderId);
$(sectionSubheaders).each(function(){
const sectionSubheaderId = $(this).attr('id');
$('<li id="sectionLink-'+sectionSubheaderId+'" class="parentElement-'+sectionHeaderId+'"><a href="#'+sectionSubheaderId+'">'+$(this).text()+'</a></li>').appendTo('#sectionLink-'+sectionHeaderId+' ul.subheaders');
});
}
});
/* Add a "Back to Top"-link while we're at it */
$('<div id="customBackToTop"><a href="#main-content" title="Back to Top">&uarr;</a></div>').appendTo('#customDocIndex');
$('#collapseCustomDocIndex').on('click', function() {
if ($(this).hasClass('collapsed')) {
$(this).addClass('folded').removeClass('collapsed');
$('#customDocIndexList > ul, #customBackToTop').hide();
$('#customDocIndex h1 .back-to-top').show();
$('#collapseCustomDocIndex').attr('aria-collapsed', false);
}
else {
$(this).addClass('collapsed').removeClass('folded');
$('#customDocIndexList > ul, #customBackToTop').show();
$('#customDocIndex h1 .back-to-top').hide();
$('#collapseCustomDocIndex').attr('aria-collapsed', true);
}
});
}
});
/* Smooth Scrolling Sticky Scrollspy Navigation:
* https://www.bram.us/2020/01/10/smooth-scrolling-sticky-scrollspy-navigation/
*/
window.addEventListener('DOMContentLoaded', () => {
const observer = new IntersectionObserver(entries => {
entries.forEach(entry => {
const id = entry.target.getAttribute('id');
if (entry.intersectionRatio > 0) {
document.querySelector(`#customDocIndex a[href="#${id}"]`).parentElement.classList.add('active');
} else {
document.querySelector(`#customDocIndex a[href="#${id}"]`).parentElement.classList.remove('active');
}
});
});
setTimeout(function (){
// Track all sections that have an `id` applied
document.querySelectorAll('section[id], section[id] h3').forEach((section) => {
observer.observe(section);
});
}, 10);
});
@hiranthi
Copy link
Author

Made some changes to the CSS to respect the active data theme for the Docs

@hiranthi
Copy link
Author

Recently Laravel introduced their own Table of Contents solution in their docs, but I like my solution better (being able to collapse it + having a back-to-top button). So I've added removing their solution to the JavaScript

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment