Last active
April 18, 2025 06:19
-
-
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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 ;-) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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; | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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">↑</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">↑</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); | |
}); |
Made some changes to the CSS to respect the active data theme for the Docs
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
An example:
https://gist.github.com/user-attachments/assets/8d4971af-7b2b-442b-8555-3ac7c612d9ee