Visual Editor - v1.1.0
Block Animations
Block animations let editors attach motion to any block that opts into
supports.artisanpackAnimations. The system covers three families:
- Entrance — plays once when the block enters the viewport (IntersectionObserver-driven).
- Hover — a transition curve + composable preset
(
lift,press,glow) on:hover. - Continuous — a CSS loop animation
(
pulse,bounce,spin,ping,wiggle,float).
It is CSS-first. The server emits a single <style data-ve-animations>
block per page that holds the @keyframes definitions plus the scoped
rules for every block on the page. A small (~3 KB gzipped) JS runtime
ships only on pages that have at least one entrance animation; it
swaps an ap-anim-pre class for ap-anim-play when the block scrolls
into view.
prefers-reduced-motion: reduce is respected by default. Editors can
opt a single block out by setting Respect reduced motion to off in
the inspector.
Attribute shape
Stored on the block as attributes.artisanpackAnimations:
{
"entrance": {
"name": "fade-in-up",
"duration": 600,
"delay": 100,
"easing": "ease-out",
"threshold": 0.2,
"once": true
},
"hover": {
"name": "lift",
"duration": 200,
"easing": "ease-out"
},
"continuous": {
"name": "pulse",
"duration": 2000,
"easing": "ease-in-out",
"count": "infinite"
},
"reducedMotion": "respect"
}
The entrance.name and continuous.name fields are
responsive-aware — pass a { base, sm, md, lg, xl, 2xl } map to enable
different motions per breakpoint, or null at a specific breakpoint to
disable the animation there:
{ "entrance": { "name": { "base": "fade-in", "md": null } } }
Registry layers
Animations are resolved in priority order, highest layer wins on key collision:
theme.json→settings.custom.artisanpack.animations- app config →
artisanpack.visual-editor.animations - package defaults →
AnimationRegistry::DEFAULTS
To remove a built-in animation, set its key to null in a higher
layer.
// config/artisanpack/visual-editor.php
'animations' => [
'entrance' => [
'flip-x' => null, // drop a built-in
'fade-in-blur' => [
'label' => 'Fade in (blur)',
'keyframe' => 'apFadeInBlur',
'duration' => 700,
'easing' => 'ease-out',
],
],
],
Custom keyframes
Hosts and themes can author named @keyframes blocks two ways.
Config — declare them on the package config:
'keyframes' => [
[
'name' => 'confetti',
'stops' => [
[ 'at' => '0%', 'transform' => 'translateY(0)' ],
[ 'at' => '50%', 'transform' => 'translateY(-12px) rotate(10deg)' ],
[ 'at' => '100%', 'transform' => 'translateY(0)' ],
],
],
],
Site Editor UI — open Styles → Animations and use the Custom
Keyframe editor. Authored keyframes persist into the Global Styles
JSON (styles.custom.artisanpack.keyframes) and reappear on reload.
Allowed stop properties: transform, opacity, filter, color,
background-color, box-shadow. Built-in keyframe names (apFadeIn,
apPulse, etc.) are reserved.
Renderer integration
The Blade renderer exposes
AnimationMarkupResolver::resolve( $scope, $attributes ) which returns
the wrapper classes, the data attributes, and the scoped CSS the
partial drops onto the block. Per-block CSS is funnelled through
AnimationCssAccumulator, which drains once at the top of every
<x-ve-blocks> render into a single <style data-ve-animations> tag
plus a <noscript> reveal block.
The React and Vue renderers expose a framework-agnostic
resolveAnimationMarkup( attributes ) that returns the same class list
- data-attribute shape so the runtime sees identical markup regardless of renderer.
Runtime
Loaded once per page (@artisanpack-ui/visual-editor/animations/runtime).
On load it:
- Reads
prefers-reduced-motion. If reduce is set, suppresses entrance animations unless the block setsdata-ap-anim-reduced="allow". - Builds one
IntersectionObserverper requested threshold; observes every[data-ap-anim-entrance]element. - When a block enters the viewport at its configured threshold,
removes
ap-anim-preand addsap-anim-play, which carries the realanimationshorthand. - By default unobserves after one play. Blocks with
data-ap-anim-once="false"are rearmed when they leave the viewport. - Watches
MutationObserverfor entrance blocks that arrive after bootstrap (e.g. revealed by an accordion expand) and observes them the same way.
Target bundle size: <5 KB gzipped. The runtime is intentionally
side-effecting at import time, so a <script type="module"> tag is
all the renderer needs.
Accessibility
prefers-reduced-motion: reduceis respected by default at both the CSS layer (a@mediablock resetsanimation+transitiontononeandopacity/transformto their final state) and the JS layer (the runtime skips IntersectionObserver setup).- A
<noscript>rule reveals every entrance block in its final state when JS doesn't run. - The inspector panel surfaces the reduced-motion preference per block so a designer can intentionally allow motion on essential animations.
Testing
- Pest unit suites:
tests/Unit/VisualEditor/Animations/* - Pest feature suite:
packages/visual-editor-renderer-blade/tests/Feature/AnimationMarkupResolverTest.php - Vitest:
resources/js/visual-editor/animations/__tests__/* - Playwright spec (deactivated until the runner is wired in CI):
tests/E2E/animations.spec.ts