Reveal
Content that fades and slides into place the first time it is scrolled into view, added with v-reveal.
Install
If this is the first component in the project, set up once. This writes ermine.json and copies the theme stylesheet. The installation guide covers Vue, Nuxt and Astro.
npx ermineui initpnpm dlx ermineui inityarn dlx ermineui initbunx ermineui initThen add the component:
npx ermineui add revealpnpm dlx ermineui add revealyarn dlx ermineui add revealbunx ermineui add revealThat copies 3 files into src/components/ui/reveal/. Files you already have are kept.
Use
import { vReveal } from '@/components/ui/reveal'In Nuxt, with the auto-import setting, the tags work in templates without the import. In Astro, use it inside a Vue island. The props are documented in the source below, on each one.
What gets copied
| File | Where it goes |
|---|---|
directive.ts | src/components/ui/reveal/directive.ts |
index.ts | src/components/ui/reveal/index.ts |
reveal.css | src/components/ui/reveal/reveal.css |
The folders are the suggested ones. ermine.json says where yours are.
Source
This is exactly what the command copies. To install by hand, create each file below and the helpers listed above.
directive.ts 44 lines
import type { Directive } from 'vue'
/**
* v-reveal — fades an element in the first time it scrolls into view.
*
* <section v-reveal>…</section>
* <li v-for="(item, index) in items" v-reveal="index * 80">…</li> each a little after the last
*
* The value is a delay in milliseconds. Elements already on screen when the page loads are left
* alone, so nothing above the fold flashes out and back in.
*/
let observer: IntersectionObserver | undefined
function watcher(): IntersectionObserver {
observer ??= new IntersectionObserver(
(entries) => {
for (const entry of entries) {
if (!entry.isIntersecting) continue
;(entry.target as HTMLElement).dataset.reveal = 'shown'
// Once is enough: it does not hide again when scrolled past.
observer!.unobserve(entry.target)
}
},
// A little of it has to be showing before it starts, so the movement is actually seen.
{ threshold: 0.15, rootMargin: '0px 0px -5% 0px' },
)
return observer
}
export const vReveal: Directive<HTMLElement, number | undefined> = {
mounted(el, binding) {
if (typeof IntersectionObserver === 'undefined') return
const box = el.getBoundingClientRect()
const onScreen = box.top < window.innerHeight && box.bottom > 0
if (onScreen) return
if (binding.value) el.style.setProperty('--er-reveal-delay', `${binding.value}ms`)
el.dataset.reveal = 'waiting'
watcher().observe(el)
},
unmounted(el) {
observer?.unobserve(el)
},
}index.ts 5 lines
/** Reveal — content that fades and slides into place the first time it is scrolled into view, added with v-reveal. */
import './reveal.css'
export { vReveal } from './directive'reveal.css 24 lines
@layer components {
/* Waiting below the fold: invisible and nudged down. The directive only sets this in a browser
that can run it, so without JavaScript (or on the server) everything is simply visible. */
[data-reveal='waiting'] {
opacity: 0;
transform: translateY(var(--er-reveal-distance, 1rem));
}
[data-reveal='shown'] {
transition:
opacity 500ms var(--er-ease, ease-out) var(--er-reveal-delay, 0ms),
transform 500ms var(--er-ease, ease-out) var(--er-reveal-delay, 0ms);
}
/* No movement for anyone who has asked for less of it: the content is just there. */
@media (prefers-reduced-motion: reduce) {
[data-reveal] {
opacity: 1;
transform: none;
transition: none;
}
}
}