Baseplate-Admin

How I recreated svelte blur effect using alpinejs and tailwind.css

15 min read

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.

  1. 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 both opacity and blur.
  2. Then we are toggling state ( so that transition is started ) by binding x-show directive. ( refer to the alpine.js docs for how x-show works )
  3. When the transition starts. We are changing the opacity: 0.5; and at the same time adding filter: blur(4px); to the element.
  4. Then when the transition ends, we are shifting from opacity: 0.5; to opacity: 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:

  1. duration of 400
  2. easing is cubic-in-out
  3. 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.


Baseplate-Admin

I’m Baseplate-Admin, a software engineer based in Middle of Nowhere.