Motion and Reactivity
Nue lets you build dynamic, motion-enriched websites with nothing but CSS. However, sometimes a bit of JavaScript can significantly enhance the user experience. Depending on what you want to build, Nue lets you choose the most suitable technology for the job: CSS, Web Component, reactive island, isomorphic component, or vanilla JavaScript.
CSS
Over the years CSS has evolved from static styling utility to an immensely powerful UX development language. Things like tooltips, dialogs, sliders, and popups no longer require JavaScript and are best implemented with CSS.
CSS offers better hardware acceleration than JavaScript and a simpler, more standards-based programming model. There are no extra layers or third-party idioms in the way. Even the more advanced stuff from libraries like Framer Motion can be implemented with modern CSS.
View transitions
One of the most significant features of Nue is the built-in support for view transitions. That is: The loading of the page and its assets are internally controlled with JavaScript and the view transition can be customized with CSS ::view-transition property. This website, for example, has this simple CSS rule for the page switch transition effect:
/* scale down the previous page */
::view-transition-old(root) {
transform: scale(.8);
transition: .4s;
}
View transitions are enabled in the site.yaml
file as follows:
view_transitions: true
Note
Future versions of Nue will also support view transitions in single-page applications. The user can seamlessly switch between the content-focused pages and the views of the single-page application and experience the transition effect defined on your design system.
Menus and dialogs
Today popovers, dialogs, and burger menus can be natively implemented with the Popover API and page-transitions can be styled with the CSS @starting-style at-rule:
[popover] {
/* final style when the transition is over */
transform: scaleX(1);
opacity: 1;
&::backdrop {
background-color: #0005;
backdrop-filter: blur(4px);
transition: .5s;
}
/* styles when the popover is about to be displayed */
{
transform: scaleX(0);
opacity: 0;
&::backdrop {
background-color: #0001;
backdrop-filter: blur(0);
}
}
}
That's all. No JavaScript is needed, the code looks clean, and all the necessary popover features and animations are in place, including keyboard support for the ESC-key.
Scroll linked transitions
Parallax effects, progress bars, image movements and skews, and other scroll-linked animations no longer require JavaScript, and can be implemented with native CSS keyframes and animation-timeline property. The front page of this website, for example, has the following animation defined for the hero image:
progress {
from { transform: scaleX(0) }
to { transform: scaleX(1) }
}
This animation is then bound to the progress of the scroll as follows:
.progress {
animation-timeline: scroll();
animation: progress;
}
Again, this was a super simple and clean syntax for defining a scroll-linked animation that would be a rather large development effort with JavaScript.
Web Components
Loading a heavy front-end library is not always the best choice for simple reactivity. It's often better to go with Web Components because they are mounted natively by the browser and are easy to write for simple things.
Simple enhancements
Web Components are great for simple things that progressively enhance the HTML markup that is already present on the document. For example, the Zen Mode
-toggle on this documentation area is a simple checkbox whose behavior is implemented as a Web Component by binding the behavior to the element with the is
attribute:
<input type="checkbox" is="zen-toggle">
The input behavior is implemented in a JavaScript file (with a .js extension) as follows:
class ZenToggle extends HTMLInputElement {
constructor() {
super()
this.onchange = function() {
document.body.classList.toggle('zen', this.checked)
}
}
}
customElements.define('zen-toggle', ZenToggle, { extends: 'input' })
One major benefit of using a Web Component is that the browser automatically takes care of component mounting and you have hooks for cleaning up resources when the component is removed from the DOM. They work nicely together with view transitions without extra coding for setting things up.
Dynamic sections
You can turn all the page sections into web components with a section_component
configuration option. This can be assigned in the front matter or globally in the application data. On the front page of this website, for example, we have a scroll-transition
component to help implement all the scroll-triggered CSS transitions:
section_component: scroll-transition
The Web component uses an Intersection Observer API for assigning an in-viewport
class to the section element whenever the user scrolls into it.
const observer = new IntersectionObserver(entries => {
entries.forEach(el =>
el.target.classList.toggle('in-viewport', el.isIntersecting)
)
}, { rootMargin: '-100px' })
class ScrollTransition extends HTMLElement {
constructor() {
super()
observer.observe(this)
}
disconnectedCallback() {
observer.unobserve(this)
}
}
customElements.define(
'scroll-transition', ScrollTransition, { extends: 'section' }
)
After this, you can develop whatever CSS transitions you wish using the in-viewport
class name. For example:
/* initial state for all section descendants */
section > * {
transition: .5s;
transform: translateY(2em);
opacity: 0;
&:nth-child(2) { transition-delay: .2s }
&:nth-child(3) { transition-delay: .5s }
&:nth-child(4) { transition-delay: .7s }
&:nth-child(5) { transition-delay: .8s }
}
/* styling when a user enters the viewport */
.in-viewport > * {
transform: translate(0);
opacity: 1;
}
Dynamic grid items
Similar to section dynamics, you can also turn your grid items into web components. This happens with a grid_item_component
configuration option, which can be assigned in the front matter, globally in site.yaml
, or for a specific area. Here, for example, we turn the grid items into dynamic gallery items.
grid_item_component: gallery-item
Custom Markdown extensions
You can implement custom Markdown extensions with web components. Here's a simple counter component:
class Counter extends HTMLDivElement {
constructor() {
super()
this.innerHTML = ++sessionStorage.counter || (sessionStorage.counter = 0)
}
}
customElements.define('view-counter', Counter, { extends: 'div' })
After this, we can use this component in a Markdown file:
[]
Reactive components
More complex components with dynamically generated HTML are better implemented with a reactive component. These components support the same template syntax as the server-side components, but the components can respond to user input.
Islands of Interactivity
Reactive islands are interactive components within the server-rendered, static HTML. This progressively rendering pattern is called the islands architecture. On this website, we have join mailing list
islands, that are implemented as follows:
<div ="join-list">
<h4 :if="sessionStorage.joined">
You have successfully joined the mailing list. Thank you for your interest!
</h4>
<form :else .prevent="submit">
<p :if="desc">{ desc }</p>
<input type="email" name="email" placeholder="Your email" required>
<textarea name="comment" placeholder="Feedback (optional)"></textarea>
<button class="secondary">{ cta || 'Join mailing list' }</button>
</form>
<script>
submit({ target }) {
const data = Object.fromEntries(new FormData(target).entries())
fetch('/public/members', {
'Content-Type': 'application/json',
body: JSON.stringify(data),
method: 'POST',
})
// change the state
sessionStorage.joined = true
}
</script>
</div>
After saving the component to a file with .htm
or .nue
extension, you can use it in your Markdown content as follows:
>[join-list cta="Submit form"]
The component can also be used on your layout files:
<join-list cta="Submit form"/>
Nue mounts reactive components automatically and hot-reloads them if you make changes. The dynamics are powered by a tiny, 2.5kb Nue.js script.
Isomorphic components
Isomorphic components are hybrid client-side and server-side components that are crawlable by search engines. For example, this website uses a video component with the following layout on the server side:
<!-- isomorphic video component utilizing Bunny CDN -->
<figure class="video" ="bunny-video">
<!-- client-side video player -->
<bunny-player :videoId="videoId" :poster="poster" :width="width"/>
<!-- caption (SEO) -->
<figcaption :if="caption">{ caption }</figcaption>
<!-- when JavaScript is disabled -->
<noscript>
<video type="video/mp4" controls
src="https://video.nuejs.org/{videoId}/play_720p.mp4">
</noscript>
</figure>
The <bunny-player>
is a reactive component defined in @lib/video.htm file, which implements simple quality detection and adaptive bitrate streaming for browsers supporting the technology.
Plain JavaScript
Not all reactivity requires a component and is better implemented with a simple JavaScript function.
Global event handlers
Sometimes you want to run JavaScript when a certain user clicks, scrolls, or keyboard event happens. This website, for example, has a global click handler that monitors user clicks and when the click target is a link nested inside a popover, the popover is closed:
addEventListener('click', e => {
const el = e.target
// hide popover menus
const menu = el.closest('[popover]')
if (menu && el.matches('a')) menu.hidePopover()
})
Google Analytics
Google Analytics and other scripts that must be imported externally should go to the head section of your website. This happens by adding a custom head
element to a root level layout file:
<head>
<script async src="//www.googletagmanager.com/gtag/js?id=G-xxxxxxx"></script>
<script client>
window.dataLayer = window.dataLayer || []
function gtag(){ dataLayer.push(arguments) }
gtag('js', new Date())
gtag('config', 'G-xxxxxxx')
</script>
</head>
Please replace the G-xxxxxxx
with your tracking ID.