How I recreated svelte
blur
effect using alpinejs
and tailwind.css
Table of contents
It's not a surprise that both svelte
and alpine.js
are both popular framework. It's no surprise that both framework has some quirks up it's sleeve.
svelte
offers a lightweight framework ( which can suffice even the most complex of features ) which adds syntactic sugar to make the developer's job easier.alpine
is a rugged, minimal tool for composing behavior directly in your markup. It adds some html directives to supercharge a MPA ( multi page application ).
We want to be greedy and get harness both of their features. More specifically we want to imitate awesome blur
tranistion from svelte
into an alpine.js
powered application.
This article requires you to have basic knowledge of alpine.js
and tailwindcss
. Essentially the article assums that you are already familiar with basics of alpine.js
and tailwindcss
.
Now that we have that out of the way, let's get started.
How to replicate the transition using alpine.js
¶
Essentially by reverse engineering a svelte powered website ( in our case a custom repl ) we can recreate what svelte
is doing ( fireship has a great video on how to reverse engineer css ).
Here's the html
( note that we are using tailwind
) that can be used to emulate ( or closely simulate ) the svelte
transition ( here's the original github source for this html ) :
<div
class="transition-all duration-400 ease-in-out"
x-show="$shown"
x-transition.duration.400ms
x-transition:enter-start="opacity-50 blur-sm"
x-transition:enter-end="opacity-100 blur-none"
x-transition:leave-start="opacity-100 blur-none"
x-transition:leave-end="opacity-50 blur-sm"
/>
How did we mimic the svelte
transition with alpine.js
¶
"Let's start by saying CSS animations are hard."
The animation is actually a 4 stage process:
When x-show
is true
the component begins the enter-start
stage. During this stage, the component will shift from its current transition state, to enter-start
for the duration ( in our case 150ms ) of the transition and then to the enter-end
stage once it's complete.
When enter-end
stage is complete, the component will shift from enter-end
to leave-start
for the duration ( again 150
ms ) of the transition and then to leave-end
stage once it's complete. Finally ending the transition.
Now that we have that out of the way let's dive deeper.
-
We are adding
transition-all
( refer to tailwind docs to see what this class adds ) to the parent element. This ensures that we are doing transition on bothopacity
andblur
. -
Then we are toggling state ( so that transition is started ) by binding
x-show
directive. ( refer to the alpine.js docs for howx-show
works ) -
When the transition starts. We are changing the
opacity: 0.5;
and at the same time addingfilter: blur(4px);
to the element. -
Then when the transition ends, we are shifting from
opacity: 0.5;
toopacity: 1;
and at the same time removing blur ( remember that our effect is to meant the animation to fade in and out | This essentially makes the old element look like they blurred out )
... Repeat it from the opposite for the new element
How does svelte
handle tranisiton¶
Let's take a look at this code ( example repl
):
<script lang='ts'>
import { blur } from 'svelte/transition';
let visible =true
</script>
<label>
<input type="checkbox" bind:checked={visible}>
visible
</label>
{#if visible}
<div transition:blur="{{amount: 10}}">
blurs in and out
</div>
{/if}
If we take a look at the JS output :
/* App.svelte generated by Svelte v3.59.1 */
import {
SvelteComponent,
add_render_callback,
append,
attr,
check_outros,
create_bidirectional_transition,
detach,
element,
empty,
group_outros,
init,
insert,
listen,
safe_not_equal,
space,
text,
transition_in,
transition_out,
} from 'svelte/internal';
import { blur } from 'svelte/transition';
function create_if_block(ctx) {
let div;
let div_transition;
let current;
return {
c() {
div = element('div');
div.textContent = 'blurs in and out';
},
m(target, anchor) {
insert(target, div, anchor);
current = true;
},
i(local) {
if (current) return;
add_render_callback(() => {
if (!current) return;
if (!div_transition)
div_transition = create_bidirectional_transition(
div,
blur,
{ amount: 10 },
true
);
div_transition.run(1);
});
current = true;
},
o(local) {
if (!div_transition)
div_transition = create_bidirectional_transition(
div,
blur,
{ amount: 10 },
false
);
div_transition.run(0);
current = false;
},
d(detaching) {
if (detaching) detach(div);
if (detaching && div_transition) div_transition.end();
},
};
}
function create_fragment(ctx) {
let label;
let input;
let t0;
let t1;
let if_block_anchor;
let current;
let mounted;
let dispose;
let if_block = /*visible*/ ctx[0] && create_if_block(ctx);
return {
c() {
label = element('label');
input = element('input');
t0 = text('\n\tvisible');
t1 = space();
if (if_block) if_block.c();
if_block_anchor = empty();
attr(input, 'type', 'checkbox');
},
m(target, anchor) {
insert(target, label, anchor);
append(label, input);
input.checked = /*visible*/ ctx[0];
append(label, t0);
insert(target, t1, anchor);
if (if_block) if_block.m(target, anchor);
insert(target, if_block_anchor, anchor);
current = true;
if (!mounted) {
dispose = listen(
input,
'change',
/*input_change_handler*/ ctx[1]
);
mounted = true;
}
},
p(ctx, [dirty]) {
if (dirty & /*visible*/ 1) {
input.checked = /*visible*/ ctx[0];
}
if (/*visible*/ ctx[0]) {
if (if_block) {
if (dirty & /*visible*/ 1) {
transition_in(if_block, 1);
}
} else {
if_block = create_if_block(ctx);
if_block.c();
transition_in(if_block, 1);
if_block.m(if_block_anchor.parentNode, if_block_anchor);
}
} else if (if_block) {
group_outros();
transition_out(if_block, 1, 1, () => {
if_block = null;
});
check_outros();
}
},
i(local) {
if (current) return;
transition_in(if_block);
current = true;
},
o(local) {
transition_out(if_block);
current = false;
},
d(detaching) {
if (detaching) detach(label);
if (detaching) detach(t1);
if (if_block) if_block.d(detaching);
if (detaching) detach(if_block_anchor);
mounted = false;
dispose();
},
};
}
function instance($$self, $$props, $$invalidate) {
let visible = true;
function input_change_handler() {
visible = this.checked;
$$invalidate(0, visible);
}
return [visible, input_change_handler];
}
class App extends SvelteComponent {
constructor(options) {
super();
init(this, options, instance, create_fragment, safe_not_equal, {});
}
}
export default App;
we can see that svelte
is calling create_bidirectional_transition
under the hood. Which just adds a style=animation: ${time} linear 0ms 1 normal both running
tag and a svelte
specific class that specifies the tranistion type to the html component ( in our case the blur
effect ).
Under the hood svelte rapidly switches between the 4 stage animation which we can see here :
The first enter
stage is in this function :
i(local) {
if (current) return;
add_render_callback(() => {
if (!current) return;
if (!div_transition)
div_transition = create_bidirectional_transition(
div,
blur,
{ amount: 10 },
true
);
div_transition.run(1);
});
current = true;
}
The second leave
stage is in this function :
o(local) {
if (!div_transition)
div_transition = create_bidirectional_transition(
div,
blur,
{ amount: 10 },
false
);
div_transition.run(0);
current = false;
}
With this we can conclude that svelte is also running ( albeit with less hassle ) a 4 stage animation ( like the one we created before with alpine.js ).
Then what the heck does import { blur } from 'svelte/transition';
do ?
Lets refer to the source
export function blur(
node: Element,
{
delay = 0,
duration = 400,
easing = cubicInOut,
amount = 5,
opacity = 0,
}: BlurParams = {}
): TransitionConfig {
const style = getComputedStyle(node);
const target_opacity = +style.opacity;
const f = style.filter === 'none' ? '' : style.filter;
const od = target_opacity * (1 - opacity);
const [value, unit] = split_css_unit(amount);
return {
delay,
duration,
easing,
css: (_t, u) =>
`opacity: ${target_opacity - od * u}; filter: ${f} blur(${
u * value
}${unit});`,
};
}
We can see that svelte
is running a:
- duration of 400
- easing is
cubic-in-out
- With optional control of opacity
Overall pretty close to our alpinejs
implementation.
Conclusion¶
While the transition is not perfect ( nothing is ), it gives developers a taste of what's possible with alpine.js
.
The vast majority of the web is based on MPA
and alpine.js
is progressively enhancing those MPA
's with SPA
like features.
Developers should add more eye candy into their good ol' dandy website instead of chasing after the shiny new JS frameworks.