Overcoming styling frustrations caused by Astro islands and slots
After using Astro for a while, I came to realize that Astro's biggest features — islands and slots — both delight and frustrate me.
Most people already know what the delights are, so I won't bother writing about them in this article. I'll focus on what frustrates me and how I resolve those frustrations.
First, we need to talk about when Astro creates islands and slots.
When Astro creates islands and slots
Astro creates an astro-island
tag, along with style
and script
tags when you include a component with client directives.
---
import Component from './components/Component.svelte'
---
<Component client:load />
If the component (with client directives) contains a slot
, Astro will create an astro-slot
tag. This astro-slot
will be found within an astro-island
.
---
import Component from './components/Component.svelte'
---
<Component client:load>
<div>Some Slotted Content</div>
</Component>
Now, if you include a component without client directives, Astro will not create astro-island
and astro-slot
tags.
---
import Component from './components/Component.svelte'
---
<Component>
<div>Some Slotted Content</div>
</Component>
What's frustrating about Astro islands and slots
Since Astro only creates these tags when you include client directives, styling components can become unpredictable — because some components will have client directives while others won't.
Problems arise when the DOM contains astro-island
and astro-slots
. That's because these tags change the document flow, so you cannot pretend that they don't exist.
To be more specific, I noticed four extremely frustrating things when astro-island
and astro-slot
are present in the DOM.
- Direct descendant selectors no longer work
- Lobotomized owls no longer work
- CSS Grid positioning no longer works
- Nth-child no longer works
I'm going to talk about each one in turn, and how to resolve these frustrations.
Direct descendant selectors no longer work
Direct descendant selectors no longer work when astro-island
and astro-slot
is in the DOM.
This makes sense because the DOM has changed, so direct descendant selectors no longer target the element you wish to target.
Imagine you want to have this HTML
<div class="Component">
<div>Some Content</div>
</div>
But Astro creates this HTML because it has slots.
<div class="Component">
<astro-slot>
<div>Some Content</div>
</astro-slot>
</div>
If you wrote a direct descendant selector, that selector wouldn't work. That's because the direct descendant selector now targets the astro-slot
level children instead of the one you are aiming for.
/* No longer works */
.Component > div {
/* Styles here */
}
Fixing it
Fixing direct descendant selectors with slots is simple — all you have to do is add astro-slot
in the selector chain.
/* Works */
.Component > astro-slot > div {
/* Styles here */
}
The above code snippets assume you're writing global CSS. If you're using CSS Scoped to the component, you'll have to write the following instead since we're dealing with slots.
/* Scoped CSS*/
.Component :global(> astro-slot > div) {
/* Styles here */
}
Another alternative is simply to use descend selectors instead of direct descendant selectors if the HTML structure allows for it.
/* Works */
.Component div {
/* Styles here */
}
Let's move on.
Lobotomized owls no longer work
Lobotomized owl is a way to style things with the sibling universal selectors. It was first coined by Heydon Pickering in 2014.
/* Lobotomized owl selector */
* + * {
/* Your styles here */
}
The lobotomized owl selector can be used to style children elements easily — giving them margins, paddings, and other properties as necessary.
Here's an example that I commonly use to give some space between elements.
.Parent > * + * {
margin-top: 1rem;
}
Unfortunately, these styles won't work on astro-island
and astro-slot
tags because they use the display: content
property.
Fixing it
Elements with display: contents
will have their styles ignored, so any styles added to these elements will be ignored.
The easy way to fix this is to add the styles to the elements contained in astro-island
or astro-slot
.
Here's what the CSS looks like.
.Parent > * + *,
.Parent > * + :where(astro-island, astro-slot) > *:first-child {
margin-top: 1rem;
}
CSS Grid Positioning no longer works
This one is similar to the lobotomized owl one — because Astro islands and slots use display: contents
, no styles will work on them.
These styles include grid-column
, grid-row
. So you will not be able to change the Conponent's positioning with grid-column
.
Here's an example where we laid all items out in a two-column grid. In this example, trying to use set grid-column
on the astro-island
and astro-slots
will not work.
---
import Component from './Component.svelte'
---
<div class='Grid'>
<Component client:load />
<Component client:load />
<Component client:load />
</div>
<style>
.Grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1em;
}
/* Tries to make all grid items span the full width */
.Grid > * {
grid-column: 1 / -1;
}
</style>
There are two ways to fix this problem.
Fixing it
The first way is to bypass astro-island
and astro-slot
with the technique mentioned above.
.grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1em;
}
/* Tries to make all grid items span the full width */
.grid > *,
.grid > :where(astro-island, astro-slot) > *:first-child {
grid-column: 1 / -1;
}
The second way is to put the components in another element.
<div class="Grid">
<div><Component client:load /></div>
<div><Component client:load /></div>
<div><Component client:load /></div>
</div>
.Grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1em;
}
.Grid > * {
grid-column: 1 / -1;
}
Nth-child no longer works as expected
When Astro adds astro-island
to the DOM, they also add style
and script
tags to the DOM at the same time.
Since style
and script
tags are also considered children elements, you cannot depend on the nth-child
selector to target the right element anymore.
Using the same code examples from above, let's say we have the following HTML.
<div class="Grid">
<Component client:load></Component>
<Component client:load></Component>
<Component client:load></Component>
</div>
This produces a DOM that looks like this.
- First element is a
style
tag - Second element is a
script
tag - Next three elements are
astro-island
tags
Astro will only include style
and script
tags for the components once in the DOM. This is why you see only one style
tag and one script
instead of 3 style
tags and 3 script
tags.
If you want to get the first component with nth-child
, you need to pass in nth-child(3)
instead of nth-child(1)
. That's because the first component is now the third element in the DOM tree.
/* Style the first component, but it's the third child */
.Grid > *:nth-child(3) > .Component {
background-color: red;
}
Yes, I know it's confusing.
There are two ways to fix this confusing problem
Fixing it
The first way is to wrap the components with another element.
You can then use nth-child
to style with a descendant selector to style the component.
<div class="Grid">
<div><Component client:load /></div>
<div><Component client:load /></div>
<div><Component client:load /></div>
</div>
/* Tries to make all grid items span the full width */
.Grid > *:first-child .Component {
background-color: red;
}
The second way is to stop using nth-child
and use nth-of-type
instead.
.Grid > astro-island:nth-of-type(1) > .Component {
background-color: red;
}
A deeper layer of frustrations
Things get a little bit more confusing if you need to pass a component into a slot — especially if both components (the parent one and the one in the slot) need JavaScript functionality.
---
import Component from './components/Component.svelte'
import Nested from './components/Nested.svelte'
---
<Component client:load>
<Nested client:load />
</Component>
Astro will create the following layout:
astro-island
in the top levelastro-slot
in the second level (sinceComponent
gets content through a slot)- Another
astro-island
afterastro-slot
sinceNested
needs to have JavaScript functionality as well.
I'm not sure whether this level of complexity is necessary though.
Most of the time, I just one layer of astro-island
and a layer of astro-slot
. So this should be an edge case more than anything else.
Wrapping up
I've just shared with you when and how Astro creates astro-island
and astro-slot
.
I've also shared with you how to overcome the styling frustrations that happen when astro-island
and astro-slot
elements are present in the DOM.
With this, you should now be able to use Astro effectively without encountering further styling issues.
Hope you find this useful in your coding journey.
If you wish to receive more in-depth articles about Astro, Svelte, and other web development topics, feel free to sign up for my newsletter below.
That's it for today. Thanks for reading!
By the way, this article is originally written on my blog. Feel free to visit that if you want to have these articles delivered to your email first-hand whenever they're released! 🙂.