After using Astro for a while, I realized that Astro’s biggest features — islands and slots — both delight and frustrate me.
Most people know the delights, 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-islandand 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. Because these tags change the document flow, you cannot pretend 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 will talk about each one 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 aim 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 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 style child 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 styleand 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 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
<img src='https://zellwk.com/images/blog/astro-styling-frustrations/astro-components.png' />
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 three style tags and three script tags.
If you want to get the first component with nth-child, you need to pass it 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 slightly 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 level
- astro-slot in the second level (since Component gets content through a slot)
- Another astro-island after astro-slot since Nested 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 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 be able to use Astro effectively without encountering further styling issues.
I 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 was originally written on my blog.
Overcoming Styling Frustrations Caused by Astro Islands and Slots was originally published in Better Programming on Medium, where people are continuing the conversation by highlighting and responding to this story.