J
Jim Bridger
Guest
A big thankyou to epicenter for sponsoring my open source work.
Earlier this week, I woke up with a thought: would it be possible to automate transitions between components purely by using element IDs, in a similar way to how View Transitions or Magic Move work? The catch was, I specifically did not want to write ANY transition logic. At all.
I figured that in order to transition between multiple elements as if they were one, they would need to have a persistent container that never left the page, and I would need to swap the relevant elements in and out.
So I would need to rip the elements out of a bunch of components, and store them somehow, ready for use. Oh, and I wanted to keep any reactivity intact.
My first thought was, what if I pass the components into a Stage component through its children snippet, and then instead of rendering the snippet, get inside it somehow and take the elements?
Since you normally render snippets using
Ok, so
...and wouldn't you know it, this gives us a Node, or a DocumentFragment - exactly what we need. Easy!
From here, I scanned through every element, (helpfully separated per component by comment nodes, thanks Svelte) and added each id to a set. Then I created persistent containers (which I'll call marks) from this set.
Here was the real challenge though: How do you swap elements in and out of your DOM while transferring their styles to a parent element and ensuring everything looks the way it ought to?
Well, you can do it manually if you want. I did. I even made Show & Svelte's first video this way. But it's incredibly brittle, it breaks some reactivity, and in general it's just a Rube Goldberg machine with an arsenal of edge cases to sting you with. So let's skip over the entire ordeal and keep this article on track. If you want to see what it looked like, here you go.
Fast forward a couple of days, and I was still thinking about snippets. Since
Unfortunately, anyone who's tried using Svelte to this level has probably also noticed something. A lot of this stuff is fairly conservatively documented:
You just read the entirety of the createRawSnippet documentation.
For starters, what the above doesn't tell you is that the
Eventually, after reading through various discussions, issues and pull requests on the svelte github, and pasting svelte source code into Claude and asking him to dumb it down for me, I learned how to create reactive snippets with
As best as I can figure out, in the above example, we need to wrap div in
However, in our case below, if we made our root element reactive in the original scene component, then it already exists as an effect dependency somewhere, and its reactivity will be preserved even if we move it, because effect dependencies are stored by node reference.
Also note that we're not trying to add any new reactive dependencies here, like putting
Hopefully that's at least partially correct. I set up this playground to test my understanding and it seems ok.
So now we have a way to render our scene components' root elements inside their marks, and it appears to be capable of preserving the reactivity we set up. It feels like we're close to a solution.
For clarity, here's an example structure:
Next we need something that we can append our scene element to in
So we put a div in
Or rather, we're going to transfer our scene element's ID and className to the mark, which will achieve the same thing:
The final code, minus boring stuff like type checking.
And with that, we have seamless transitions between elements in different components, without needing to write any transition logic at all. Pretty sweet! Our components are just a collection of root elements with IDs that we can style internally with scoped styles, and then pass as children into our Stage.
Here's a short video of what all of that looks like, made with Show & Svelte of course.
Snippet gang rise up!
Show & Svelte is 100% open source, MIT Licensed, and available on github and npm. Comes with full markdown and code editing support, with syntax highlighting.
So far it's been a game changer for me making videos for YouTube, a process that I genuinely hated before. Check it out and let me know how it goes!
Discord | Github
* ...to save you a huge amount of trouble in future, remember this: if you want to transfer CSS styles from one element to another, you ONLY need to transfer the ID and the className. Not the, you know, actual styles. And you certainly don't need to render an entire offscreen stage element populated with clone elements from which you get computed styles, and then sift through which styles actually get included in computed styles, not to mention that you're now dealing with the results of the CSS calculations instead of the actual CSS and... well... ID and className. That's. All. You. Need.
Continue reading...
Earlier this week, I woke up with a thought: would it be possible to automate transitions between components purely by using element IDs, in a similar way to how View Transitions or Magic Move work? The catch was, I specifically did not want to write ANY transition logic. At all.
I figured that in order to transition between multiple elements as if they were one, they would need to have a persistent container that never left the page, and I would need to swap the relevant elements in and out.
So I would need to rip the elements out of a bunch of components, and store them somehow, ready for use. Oh, and I wanted to keep any reactivity intact.
My first thought was, what if I pass the components into a Stage component through its children snippet, and then instead of rendering the snippet, get inside it somehow and take the elements?
Since you normally render snippets using
{@render children()}
, what if I just call children in my script tag? Well, if you try it, you get this: Cannot read properties of undefined (reading 'before')
. So, the children snippet is expecting some object, which has a property called 'before'. Interesting.
Code:
children({ before: null })
// error: anchor.before is not a function
Ok, so
before
should be a function. Let's expect some arguments and see what we get.
Code:
children({ before: (...args) => console.dir(args) })
...and wouldn't you know it, this gives us a Node, or a DocumentFragment - exactly what we need. Easy!
From here, I scanned through every element, (helpfully separated per component by comment nodes, thanks Svelte) and added each id to a set. Then I created persistent containers (which I'll call marks) from this set.
Here was the real challenge though: How do you swap elements in and out of your DOM while transferring their styles to a parent element and ensuring everything looks the way it ought to?
Well, you can do it manually if you want. I did. I even made Show & Svelte's first video this way. But it's incredibly brittle, it breaks some reactivity, and in general it's just a Rube Goldberg machine with an arsenal of edge cases to sting you with. So let's skip over the entire ordeal and keep this article on track. If you want to see what it looked like, here you go.
Fast forward a couple of days, and I was still thinking about snippets. Since
createRawSnippet
exists, I thought, it must be possible to convert all those root elements into their own snippets, and then render them inside their marks.Unfortunately, anyone who's tried using Svelte to this level has probably also noticed something. A lot of this stuff is fairly conservatively documented:
Code:
function createRawSnippet<Params extends unknown[]>(
fn: (...params: Getters<Params>) => {
render: () => string;
setup?: (element: Element) => void | (() => void);
}
): Snippet<Params>;
You just read the entirety of the createRawSnippet documentation.
For starters, what the above doesn't tell you is that the
element
parameter in setup
will be the topmost element that you put inside render. Try to use createRawSnippet
and pass an empty string to render, and all you'll get is: Illegal invocation
, an uncharacteristically unhelpful error. Add an element though:
Code:
const mySnippet = createRawSnippet(() => ({
render: () => "<div id='render-div'></div>",
setup: (element) => console.log(element)
}))
// this will log the div#render-div to the console.
Eventually, after reading through various discussions, issues and pull requests on the svelte github, and pasting svelte source code into Claude and asking him to dumb it down for me, I learned how to create reactive snippets with
createRawSnippet
:
Code:
let count = $state(0)
const mySnippet = createRawSnippet(() => ({
render: () => `<div></div>`,
setup: (div) => {
$effect(() => {
div.textContent = count
})
}
})
)
As best as I can figure out, in the above example, we need to wrap div in
$effect
because it's a new div. We just created it right there in render. It isn't a dependency of any current effects.However, in our case below, if we made our root element reactive in the original scene component, then it already exists as an effect dependency somewhere, and its reactivity will be preserved even if we move it, because effect dependencies are stored by node reference.
Also note that we're not trying to add any new reactive dependencies here, like putting
count
in setup or anything. We're just moving the node.
Code:
// ourRootElement was extracted from the original snippet
const mySnippet = createRawSnippet(() => ({
render: () => `<div></div>`,
setup: (div) => {
div.appendChild(ourRootElement);
}
})
)
Hopefully that's at least partially correct. I set up this playground to test my understanding and it seems ok.
So now we have a way to render our scene components' root elements inside their marks, and it appears to be capable of preserving the reactivity we set up. It feels like we're close to a solution.
For clarity, here's an example structure:
Code:
- Slide/Scene Component 1
- <article id="title">
- Stage
- <Mark id="title">
- #snippet (the one we've been talking about making)
- <div></div> (created inside `render`)
- <article id="title"> (appended to `div` in `setup`)
Next we need something that we can append our scene element to in
setup
. This obviously can't be our mark, because that already exists; we're trying to render this created snippet inside it. We also have to create an element in render
. We'll also need a reference to our mark in just a moment. Three birds. One div.So we put a div in
render
and then in setup
, we append our element to it. Then we use div.parentNode
to get the reference to our mark which we need because we're going to transfer our scene element's styles to it*.Or rather, we're going to transfer our scene element's ID and className to the mark, which will achieve the same thing:
Code:
createRawSnippet(() => ({
render: () => "<div></div>",
setup: (div) => {
const mark = div.parentNode;
swapClass(element, mark)
element.removeAttribute("id")
div.appendChild(element);
return () => {
swapClass(mark, element)
element.id = mark.id
}
}
}))
The final code, minus boring stuff like type checking.
And with that, we have seamless transitions between elements in different components, without needing to write any transition logic at all. Pretty sweet! Our components are just a collection of root elements with IDs that we can style internally with scoped styles, and then pass as children into our Stage.
Here's a short video of what all of that looks like, made with Show & Svelte of course.
Snippet gang rise up!
Show & Svelte is 100% open source, MIT Licensed, and available on github and npm. Comes with full markdown and code editing support, with syntax highlighting.
So far it's been a game changer for me making videos for YouTube, a process that I genuinely hated before. Check it out and let me know how it goes!
Discord | Github
* ...to save you a huge amount of trouble in future, remember this: if you want to transfer CSS styles from one element to another, you ONLY need to transfer the ID and the className. Not the, you know, actual styles. And you certainly don't need to render an entire offscreen stage element populated with clone elements from which you get computed styles, and then sift through which styles actually get included in computed styles, not to mention that you're now dealing with the results of the CSS calculations instead of the actual CSS and... well... ID and className. That's. All. You. Need.
Continue reading...