Shipping less
A w.* element animates its values with nothing else: initial, animate, exit, variants and transitions. Each thing it does beyond that, a gesture, layout or a colour space, is a Feature: code a page ships only if it asks for it.
import { LazyWardic, w } from "@wardic/solid/unstable/v2"
import { createSignal, Show } from "solid-js"
/** A ball that drags once you ask: the page fetches `drag` on the press, not with itself. */
export default function Shipping() {
const [wanted, setWanted] = createSignal(false)
// How long `drag` took to arrive, from the press: absent until it has.
const [arrivedIn, setArrivedIn] = createSignal<number>()
let arena!: HTMLDivElement
let pressedAt = 0
// The section's Features, as a function: it runs as the section renders, so on the press.
const loadDrag = async () => {
const { drag } = await import("./drag-feature.ts")
setArrivedIn(Math.round(performance.now() - pressedAt))
return drag
}
const load = () => {
pressedAt = performance.now()
setWanted(true)
}
return (
<div class="sample-stack">
<button class="sample-button" disabled={wanted()} onClick={load}>
Load drag
</button>
<div class="sample-arena" ref={arena}>
<Show when={wanted()} fallback={<div class="sample-ball" />}>
<LazyWardic features={loadDrag}>
<w.div class="sample-ball" drag dragConstraints={() => arena} whileDrag={{ scale: 1.15 }} />
</LazyWardic>
</Show>
</div>
<p class="sample-note" aria-live="polite">
<Show when={wanted()} fallback={<>No drag yet: the ball stays put.</>}>
<Show when={arrivedIn() !== undefined} fallback={<>Loading drag…</>}>
Drag arrived in {arrivedIn()} ms.
</Show>
</Show>
</p>
</div>
)
}import { LazyWardic, w } from "@wardic/solid"
import { createSignal, Show } from "solid-js"
/** A ball that drags once you ask: the page fetches `drag` on the press, not with itself. */
export default function Shipping() {
const [wanted, setWanted] = createSignal(false)
// How long `drag` took to arrive, from the press: absent until it has.
const [arrivedIn, setArrivedIn] = createSignal<number>()
let arena!: HTMLDivElement
let pressedAt = 0
// The section's Features, as a function: it runs as the section renders, so on the press.
const loadDrag = async () => {
const { drag } = await import("./drag-feature-solid1.ts")
setArrivedIn(Math.round(performance.now() - pressedAt))
return drag
}
const load = () => {
pressedAt = performance.now()
setWanted(true)
}
return (
<div class="sample-stack">
<button class="sample-button" disabled={wanted()} onClick={load}>
Load drag
</button>
<div class="sample-arena" ref={arena}>
<Show when={wanted()} fallback={<div class="sample-ball" />}>
<LazyWardic features={loadDrag}>
<w.div class="sample-ball" drag dragConstraints={() => arena} whileDrag={{ scale: 1.15 }} />
</LazyWardic>
</Show>
</div>
<p class="sample-note" aria-live="polite">
<Show when={wanted()} fallback={<>No drag yet: the ball stays put.</>}>
<Show when={arrivedIn() !== undefined} fallback={<>Loading drag…</>}>
Drag arrived in {arrivedIn()} ms.
</Show>
</Show>
</p>
</div>
)
}import { LazyMotion } from "motion/react"
import * as m from "motion/react-m"
import { useRef, useState } from "react"
/** A ball that drags once you ask: the page fetches Motion's Features on the press, not with itself. */
export default function Shipping() {
const [wanted, setWanted] = useState(false)
// How long the Features took to arrive, from the press: absent until they have.
const [arrivedIn, setArrivedIn] = useState<number>()
const arena = useRef<HTMLDivElement>(null)
const pressedAt = useRef(0)
// Motion's drag comes in `domMax`, with layout: they load for the whole page.
const loadFeatures = async () => {
const features = (await import("./motion-features.ts")).default
setArrivedIn(Math.round(performance.now() - pressedAt.current))
return features
}
const load = () => {
pressedAt.current = performance.now()
setWanted(true)
}
let status = "No drag yet: the ball stays put."
if (wanted) status = "Loading drag…"
if (arrivedIn !== undefined) status = `Drag arrived in ${arrivedIn} ms.`
return (
<div className="sample-stack">
<button className="sample-button" disabled={wanted} onClick={load}>
Load drag
</button>
<div className="sample-arena" ref={arena}>
{wanted ? (
<LazyMotion features={loadFeatures}>
<m.div className="sample-ball" drag dragConstraints={arena} whileDrag={{ scale: 1.15 }} />
</LazyMotion>
) : (
<div className="sample-ball" />
)}
</div>
<p className="sample-note" aria-live="polite">
{status}
</p>
</div>
)
}The Features
| Feature | What it brings |
|---|---|
hover | whileHover, onHoverStart and onHoverEnd |
press | whileTap, onTap, onTapStart and onTapCancel, from the pointer and the keyboard |
focus | whileFocus |
inView | whileInView, viewport, onViewportEnter and onViewportLeave |
pan | onPan, onPanStart, onPanSessionStart and onPanEnd |
drag | drag, its constraints, the glide on release, and dragControls |
layout | layout, layoutId, layoutScroll and layoutRoot |
colors | oklch(), oklab(), lab(), lch(), hwb() and color(). Hex, rgb(), hsl() and colour names need nothing. |
On Solid 2, the gesture Features bring the ward's events too, such as "hover" and "tap". Motion's two sets are here under the same names: domAnimation is hover, press, focus and inView, and domMax is every Feature. Each is a list, so features={[domAnimation, layout]} works.
Where they go
<WardicProvider features>gives them to the whole app: usuallydomAnimation.<LazyWardic features>gives them to a section. Its elements have the provider's Features and the section's, and a sibling section's Features stay its own. A page that drags wraps what drags in<LazyWardic features={[drag]}>, sodragships with that page's code, and pages that don't drag never load it.- Components that need a Feature bring it:
Reorder.Itembringsdrag,layoutand the gestures, andReorder.Groupbringslayout.LayoutGroupbringslayoutfor its ownlayoutDependencyonly: a layout element under it still needslayoutfrom its section.
Loaded when it's wanted
features can be a function that loads them, as in the demo:
// drag-feature.ts holds `export { drag } from "@wardic/solid/unstable/v2"`, so the import is a chunk of its own.
<LazyWardic features={() => import("./drag-feature.ts").then((m) => m.drag)}>…</LazyWardic>It loads as the section renders, in the browser only. So a section rendered on a press loads on the press, one rendered as it scrolls into view loads then, and one rendered once the page has mounted loads after the first paint. While it loads:
- Values animate at once, and each Feature takes effect as it arrives. A drag tried before
draghas arrived does nothing, and isn't replayed. - An element with a layout prop starts laying out when
layoutarrives: it animates its next layout change. - The server loads nothing: it renders each element at its starting style, as it always does.
A Feature left out
A prop whose Feature isn't there does nothing: whileHover without hover doesn't play, drag without drag doesn't drag, and an oklch() colour without colors isn't read as a colour. In development, Wardic warns once for each Feature, naming it and where to add it.
Every Feature at once
wardic.* is the element with every Feature, whatever its section has, and so are wardic.create and the ward that isn't w's: createWard on Solid 1, ward() on Solid 2. It's handy for trying something out, and heavy for a page. strict on a WardicProvider or a LazyWardic makes one throw in development, so none reaches a page by accident.
What it saves
Measured on Solid 1's binding at 1.0.0-rc.2, gzipped, each over the Slim element alone, 122 KB for the whole app:
| Feature | Size |
|---|---|
hover | 5 KB, the input every gesture but inView stands on included |
press, focus and inView, after hover | 2 KB |
drag, after domAnimation | 8 KB |
layout | 23 KB |
colors | 5 KB |
Every Feature, a wardic.* element | 42 KB |