Fragment support on this site is being achieved with:
- JavaScript for setting up fragment rules
- CSS for animating fragment visits
- The DOM for special cases and extra functionality
JavaScript
Install swup and fragment-plugin from NPM:
The following code is being used on this very site for initializing swup with fragment support:
import Swup from "swup";
import SwupFragmentPlugin, { Rule as FragmentRule } from "@swup/fragment-plugin";
/**
* Define the rules for Fragment Plugin
*/
const rules: FragmentRule[] = [
// Rule 1: Between the various views of the characters list
{
from: "/characters/:filter?",
to: "/characters/:filter?",
containers: ["#characters-list"],
scroll: '#characters-list'
},
// Rule 2: From the list of characters to a character detail page
{
from: "/characters/:filter?",
to: "/character/:character",
containers: ["#character-modal"],
name: "open-character",
scroll: true
},
// Rule 3: From a single character back to the list of characters
{
from: "/character/:character",
to: "/characters/:filter?",
containers: ["#character-modal", "#characters-list"],
name: "close-character",
},
// Rule 4: Between characters (previous/next)
{
from: "/character/:character",
to: "/character/:character",
containers: ["#character-detail"],
},
];
/**
* Initialize Swup with Fragment Plugin
*/
const swup = new Swup({
plugins: [
new FragmentPlugin({ rules }),
],
});
Did you notice something strange about rule 3? It’s telling the plugin to
not only replace #character-modal
but also #characters-list
! Surely that’s
not we want when we are only closing an overlay, right?
Turns out: There are cases where we actually want that! When navigating from a character to a filtered list of characters that wasn’t active before we opened the character. Or when jumping multiple steps backwards or forwards in the browser history.
Fragment Plugin keeps track of the URL for each fragment element as soon as it is being rendered. On subsequent visits, the element won’t be replaced if it already matches the visit’s URL.
CSS
The CSS for the animations of fragment visits is scoped to the fragment elements:
/* The default transition for non-fragment visits */
.is-changing .transition-main {
transition-property: opacity, transform;
transition-duration: 250ms;
}
html.is-animating .transition-main {
opacity: 0;
transform: translateY(-20px);
}
html.is-leaving .transition-main {
transform: translateY(20px);
}
/*
* The transition when filtering the characters.
* Here, we are animating the `.teaeser` elements individually
*/
#characters-list.is-changing {
--duration-leave: 150ms;
--duration-enter: 400ms;
transition-duration: var(--duration-enter);
}
#characters-list.is-leaving {
transition-duration: var(--duration-leave);
}
#characters-list.is-changing .teaser {
transition-property: opacity, transform;
transition-duration: var(--duration-enter);
transition-timing-function: cubic-bezier(0.230, 1.000, 0.320, 1.000); /* easeOutQuint */
}
#characters-list.is-animating .teaser {
opacity: 0;
transform: scale(0.75);
}
/* Change easing and duration for the leave-phase */
#characters-list.is-leaving .teaser {
transition-timing-function: ease-in;
transition-duration: var(--duration-leave);
}
/*
* The animation for opening and closing the character modal
*/
#character-modal.is-changing {
transition: opacity 300ms;
}
#character-modal.is-animating {
opacity: 0;
}
/* Zoom-Effect for the character detail when opening/closing the modal */
#character-modal.is-changing #character-detail {
transition: transform 150ms;
transform: scale(1);
}
#character-modal.to-open-character.is-animating #character-detail,
#character-modal.to-close-character.is-leaving #character-detail {
transform: scale(0.95);
}
/* Animate the backdrop of the modal */
#character-modal::backdrop {
transition: opacity 300ms;
}
#character-modal.is-animating::backdrop,
.is-animating #character-modal::backdrop {
opacity: 0;
}
/*
* The animation between charcter details. Directional animation based on data-swup-animation
*/
#character-detail.is-changing {
transition: opacity 200ms, transform 200ms;
}
#character-detail.to-next {
--direction: 1;
}
#character-detail.to-previous {
--direction: -1;
}
#character-detail.is-animating {
opacity: 0;
transform: translateX(calc(40px * var(--direction)));
}
#character-detail.is-leaving {
transform: translateX(calc(40px * var(--direction) * -1));
}
DOM
[data-swup-fragment-url]
Let’s visit one of the character detail pages directly, Luigi, for example.
On that page, the #characters-list
behind the #character-modal
displays the items that would also be
rendered when navigating towards /characters/
. But there is no way for Fragment Plugin to automatically
detect that fact, so it would replace the list upon closing the modal, following our third fragment rule:
We can solve this by providing the URL of the #characters-list
beforehand, by adding
data-swup-fragment-url="/characters/"
to it. Now Fragment Plugin can correctly skip the
#characters-list
when we navigate from the open #character-modal
to /characters/
:
Learn more about this attribute
[data-swup-link-to-fragment]
Using [data-swup-link-to-fragment="<selector>"]
, you can tell any link on your site to be synced
to the matching element’s fragment URL. This is in use for the character modal’s close links on this site:
Learn more about this attribute
Modals inside transform
ed parents
Suppose you have a fragment that you want to open like a modal, above all other content. Just like on the character detail pages on this site.
This (reduced) CSS is being used to make the #item-detail
appear as a modal above everything else:
This will work fine, until we apply a transform
to any of the modal’s parent elements, as we do on this site:
The reason for this is that transform
establishes a containing block for all descendants.
You have two options to fix this:
- Don’t apply CSS
transform
s to any of the parents of a modal - Use
<detail open>
for the modal:
Fragment Plugin will detect <detail open>
fragment elements automatically on every page view and run showModal()
on them, putting them on the top layer and thus allows them to not be affected by parent element styles, anymore.
Modals and accessibility
Element order
The first <main>
element in a document will be considered the main content by assistive technology. If you are using the A11y Plugin, that’s also the element that will automatically be focused upon page visits. For that reason, you should always put your modal before any overlayed content, if it should be considered the primary content of a page.
Pros and cons of using a <dialog open>
element for modals
Pros:
- The modal’s positioning won’t be affected by
transform
animations on any of it’s parents. - Focus trapping will be natively available for the modal without you having to do anything.
Cons:
- Wrapping your
<main>
content inside a<dialog>
will produce semantically incorrect markup. We still think it’s the cleanest approach for now, until the Popover API reaches wider browser support.