Some CSS styles not applied with non-zero inlineStyleThreshold since v2.21.3
Describe the bug
Since SvelteKit v2.21.3, when inlineStyleThreshold is set to a non-zero value, some CSS styles are not being applied correctly.
Reproduction
Logs
System Info
System:
OS: macOS 15.5
CPU: (12) arm64 Apple M3 Pro
Memory: 708.78 MB / 36.00 GB
Shell: 5.9 - /bin/zsh
Binaries:
Node: 22.16.0 - ~/.volta/tools/image/node/22.16.0/bin/node
npm: 10.9.2 - ~/.volta/tools/image/node/22.16.0/bin/npm
Browsers:
Chrome: 137.0.7151.69
Safari: 18.5
npmPackages:
@sveltejs/adapter-node: 5.2.12 => 5.2.12
@sveltejs/kit: 2.21.4 => 2.21.4
@sveltejs/vite-plugin-svelte: 5.1.0 => 5.1.0
svelte: 5.33.19 => 5.33.19
vite: 6.3.5 => 6.3.5
Severity
serious, but I can work around it
Additional Information
Setting inlineStyleThreshold: 0 resolves the issue.
I can confirm this bug as I thought I was hallucinating 😅
It is not styling my Button component (which is normally imported, not dynamically) with the inlineStyleThreshold config option to 4096. It used to work a week ago and since this version, it doesn't build the style of my component when building.
At the moment to keep this working I had to lower the threshold to 1024.
@eltigerchino is this related to that dynamic component styling PR?
@eltigerchino is this related to that dynamic component styling PR?
Probably. I'll have a look and try to find a reproduction.
I can confirm this bug as I thought I was hallucinating 😅 It is not styling my Button component (which is normally imported, not dynamically) with the
inlineStyleThresholdconfig option to 4096. It used to work a week ago and since this version, it doesn't build the style of my component when building.At the moment to keep this working I had to lower the threshold to 1024.
Are you able to create a minimal reproduction? Or if you're unable to, could you share the styles? How were the styles added? In thee Svelte style block? Or imported as a CSS file in the Svelte script tag? Or how the component was imported into the page/layout?
I'm finding this difficult to reproduce at the moment. A few things I've tried:
- Inlining CSS for the page component.
- Importing a component and inlining that CSS.
- Importing a component which imports another component and inlining that CSS.
Let me know if there are other cases I should try. I wonder if CSS syntax plays a part here.
@eltigerchino Here is my component Button.svelte:
<style lang="scss" src="./Button.scss"></style>
<script lang="ts">
import type { Snippet } from 'svelte'
let {
label,
href,
type,
withIcon = false,
icon = '+',
disabled = false,
isActive = false,
isInactive = false,
children,
class: className,
onclick,
}: {
label: string | null
href?: string
type?: 'submit' | 'reset' | 'button'
withIcon?: boolean
icon?: string
disabled?: boolean
isActive?: boolean
isInactive?: boolean
class?: string
onclick?: any
children?: Snippet
} = $props()
const tag = $derived(href ? 'a' : 'button')
const isExternal = $derived(href && /^https?:\/\//i.test(href))
const classes = $derived([
'button',
'text-caps',
className,
isActive && 'is-active',
isInactive && 'is-inactive',
])
</script>
<svelte:element
this={tag}
{href}
type={tag === 'button' ? type : undefined}
class={classes}
disabled={tag === 'button' ? disabled : null}
tabindex={tag === 'button' ? 0 : null}
target={isExternal ? '_blank' : undefined}
rel={isExternal ? 'noreferrer noopener' : undefined}
data-sveltekit-noscroll={!href ? undefined : href && !isExternal ? '' : undefined}
role={tag === 'button' ? 'button' : undefined}
{onclick}
>
{@render children?.()}
{#if label}
<span class="label" data-label={label}>
<span>{label}</span>
</span>
{/if}
{#if withIcon && icon}
<span class="icon">{icon}</span>
{/if}
</svelte:element>
And styles:
.button {
--sides: 12px;
--duration: 0.35s;
--color: var(--color-dark);
position: relative;
display: inline-flex;
align-items: center;
height: 36px;
padding: 0 var(--sides);
overflow: hidden;
text-align: left;
white-space: nowrap;
color: var(--color);
line-height: 1;
text-decoration: none;
background: none;
border: none;
box-shadow: inset 0 0 0 1px var(--color);
appearance: none;
cursor: pointer;
transition-property: background-color, box-shadow, color, opacity;
transition-duration: var(--duration);
transition-timing-function: var(--ease-quart-inout);
// State: Disabled
&:disabled {
opacity: 0.25;
cursor: not-allowed;
}
// State: Hover
&:not(:disabled):not(.is-active):hover {
background-color: var(--color-dark);
color: var(--color-white);
.label {
transform: translateY(2.5em);
}
&:after {
transform: translateY(0);
}
}
// State: Pressing
&:active {
.label {
opacity: 0.65;
}
}
// State: Active
&.is-active {
background-color: var(--color-dark);
color: var(--color-white);
.label {
transform: translateY(2.5em);
}
&:after {
transform: translateY(0);
}
}
// State: Is inactive
&.is-inactive {
--color: var(--color-dark-20);
color: var(--color-dark);
}
// Label
.label {
position: relative;
transition-property: transform, opacity;
transition-duration: var(--duration);
transition-timing-function: var(--ease-quart-inout);
span {
white-space: nowrap;
}
&:after {
content: attr(data-label);
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
transform: translateY(-2.5em);
}
}
// Icon
.icon {
display: flex;
align-items: center;
height: 100%;
margin-left: var(--sides);
margin-right: calc(var(--sides) * -1 / 2);
padding-left: 0.5em;
border-left: 1px solid currentColor;
font-size: 1.25em;
font-weight: 500;
transition: border-color var(--duration) var(--ease-quart-inout);
}
}
Then imported that way, as simply as it can be:
<script lang="ts">
import Button from '$components/atoms/Button/Button.svelte'
</script>
...
<Button
withIcon
type="submit"
label={isLoading ? 'Processing…' : 'Inquire'}
disabled={isLoading}
/>
Using this config:
...
const config = {
preprocess: sveltePreprocess({
scss: {
prependData: globalStyles,
}
}),
kit: {
adapter: adapter({
routes: {
include: ['/*'],
exclude: ['<all>'],
},
}),
alias: {
$components: 'src/components',
$layouts: 'src/layouts',
$animations: 'src/animations',
$utils: 'src/utils',
$stores: 'src/stores',
},
prerender: {
handleMissingId: 'ignore',
},
inlineStyleThreshold: 4096,
// ...
}
What is weird is that this exact code that I haven't changed works and build styles correctly on 2.21.2 but not on 2.21.3 or 2.21.4
@eltigerchino Here is my component Button.svelte:
<svelte:element this={tag} {href} type={tag === 'button' ? type : undefined} class={classes} disabled={tag === 'button' ? disabled : null} tabindex={tag === 'button' ? 0 : null} target={isExternal ? '_blank' : undefined} rel={isExternal ? 'noreferrer noopener' : undefined} data-sveltekit-noscroll={!href ? undefined : href && !isExternal ? '' : undefined} role={tag === 'button' ? 'button' : undefined} {onclick}
{@render children?.()} {#if label} <span class="label" data-label={label}> <span>{label}</span> </span> {/if} {#if withIcon && icon} <span class="icon">{icon}</span> {/if}</svelte:element>
And styles:
.button { --sides: 12px; --duration: 0.35s; --color: var(--color-dark); position: relative; display: inline-flex; align-items: center; height: 36px; padding: 0 var(--sides); overflow: hidden; text-align: left; white-space: nowrap; color: var(--color); line-height: 1; text-decoration: none; background: none; border: none; box-shadow: inset 0 0 0 1px var(--color); appearance: none; cursor: pointer; transition-property: background-color, box-shadow, color, opacity; transition-duration: var(--duration); transition-timing-function: var(--ease-quart-inout);
// State: Disabled &:disabled { opacity: 0.25; cursor: not-allowed; } // State: Hover &:not(:disabled):not(.is-active):hover { background-color: var(--color-dark); color: var(--color-white); .label { transform: translateY(2.5em); } &:after { transform: translateY(0); } } // State: Pressing &:active { .label { opacity: 0.65; } } // State: Active &.is-active { background-color: var(--color-dark); color: var(--color-white); .label { transform: translateY(2.5em); } &:after { transform: translateY(0); } } // State: Is inactive &.is-inactive { --color: var(--color-dark-20); color: var(--color-dark); } // Label .label { position: relative; transition-property: transform, opacity; transition-duration: var(--duration); transition-timing-function: var(--ease-quart-inout); span { white-space: nowrap; } &:after { content: attr(data-label); position: absolute; top: 0; left: 0; width: 100%; height: 100%; transform: translateY(-2.5em); } } // Icon .icon { display: flex; align-items: center; height: 100%; margin-left: var(--sides); margin-right: calc(var(--sides) * -1 / 2); padding-left: 0.5em; border-left: 1px solid currentColor; font-size: 1.25em; font-weight: 500; transition: border-color var(--duration) var(--ease-quart-inout); }}
Then imported that way, as simply as it can be:
...
<Button withIcon type="submit" label={isLoading ? 'Processing…' : 'Inquire'} disabled={isLoading} />
Using this config:
...
const config = { preprocess: sveltePreprocess({ scss: { prependData: globalStyles, } }),
kit: { adapter: adapter({ routes: { include: ['/*'], exclude: ['<all>'], }, }), alias: { $components: 'src/components', $layouts: 'src/layouts', $animations: 'src/animations', $utils: 'src/utils', $stores: 'src/stores', }, prerender: { handleMissingId: 'ignore', }, inlineStyleThreshold: 4096, // ...}
What is weird is that this exact code that I haven't changed works and build styles correctly on 2.21.2 but not on 2.21.3 or 2.21.4
Thank you! I've tried to reproduce this by adding sveltePreprocss, sass, and the scss + svelte code but I'm not sure what I'm suppose to expect. Can you share a screenshot of what the button looks like working/broken? Also, I'm missing some of the global styles the scss references like --color-dark, so I don't know if my comparison will be accurate. However, I can see the styles inlined in the <head>. I just don't know if the issue is that the styles are not inlined or if the styles are inlined but not applying correctly?
It simply doesn't compile or applies the styles for this component for some reason. While running dev, it's all good but when building, it just renders the button as the default style 🤔
It simply doesn't compile or applies the styles for this component for some reason. While running dev, it's all good but when building, it just renders the button as the default style 🤔
We only inline styles after build so that's why it always works during dev.
I wasn't able to reproduce the styles not applying as I can see there's inlined styles after I build the app. Is there any way you can strip down your current project to share a minimal reproduction?
@eltigerchino I tried to replicate it on a smaller setup by just keeping a <Button /> in a <main> within a +page.svelte and it builds fine for some reason, with the same Svelte config. This is quite weird! I'll keep investigating but I suspect that a bigger site with many more components can cause some sort of a threshold with the inlineStyle setting?
I’m trying to reproduce the issue with the minimal setup, but I haven’t been able to reproduce it yet. I’m using SCSS for styling and not using dynamic imports.
@tkhs0813 @eltigerchino I gave another go and it seems that only one component is affected in my case, which is weird.
I think I found the issue, or at least a case where it happens: using this component in that route routes/product/[product]/+page.svelte without having a root page for /product seems to be the culprit. I literally have nothing else than <Button href="/page" label="Test" /> in the page and the styles are not being built.
Is this of any help? Or does that make any sense? 😅
The only way to make this work with inlineStyleThreshold is to lower the value to 2048.
The component in question:
<style lang="scss" src="./Button.scss"></style>
<script lang="ts">
import type { Snippet } from 'svelte'
let {
label,
href,
type,
withIcon = false,
icon = '+',
disabled = false,
isActive = false,
isInactive = false,
children,
class: className,
onclick,
}: {
label: string | null
href?: string
type?: 'submit' | 'reset' | 'button'
withIcon?: boolean
icon?: string
disabled?: boolean
isActive?: boolean
isInactive?: boolean
class?: string
onclick?: any
children?: Snippet
} = $props()
const tag = $derived(href ? 'a' : 'button')
const isExternal = $derived(href && /^https?:\/\//i.test(href))
</script>
<svelte:element
this={tag}
{href}
type={tag === 'button' ? type : undefined}
class={['button text-caps', className]}
class:is-active={isActive}
class:is-inactive={isInactive}
disabled={tag === 'button' ? disabled : null}
tabindex={tag === 'button' ? 0 : null}
target={isExternal ? '_blank' : undefined}
rel={isExternal ? 'noreferrer noopener' : undefined}
data-sveltekit-noscroll={!href ? undefined : href && !isExternal ? '' : undefined}
role={tag === 'button' ? 'button' : undefined}
{onclick}
>
{@render children?.()}
{#if label}
<span class="label" data-label={label}>
<span>{label}</span>
</span>
{/if}
{#if withIcon && icon}
<span class="icon">{icon}</span>
{/if}
</svelte:element>
Could it be because of the <svelte:element>?
EDIT: having refactored the component to use a content() snippet and conditions based on the tag, it's not that!
Another case where all the styles would build including my nested <Button /> component: when I do NOT add the stylesheet for my routes/product/[product]/+page.svelte, or have an empty stylesheet:
<style lang="scss" src="../../../styles/pages/product.scss"></style>
I feel there is a bug somewhere related to stylesheets with dynamic routes.
@eltigerchino For your reference: We still haven’t been able to reproduce the issue with a minimal setup, but we were able to resolve the missing styles problem in our application by using Cursor. https://github.com/sveltejs/kit/pull/14007
However, we're not sure if this fix is actually appropriate.
Hey just checking in, is there a hope for this to get fixed? 🙌
Sorry, this kinda got lost in the backlog for me personally as I've had to prioritise some non-SvelteKit work last month. I'll try to see if I can squeeze some time in for this this week or next (most likely).
@tkhs0813 @eltigerchino I gave another go and it seems that only one component is affected in my case, which is weird.
I think I found the issue, or at least a case where it happens: using this component in that route
routes/product/[product]/+page.sveltewithout having a root page for/productseems to be the culprit. I literally have nothing else than<Button href="/page" label="Test" />in the page and the styles are not being built.Is this of any help? Or does that make any sense? 😅
The only way to make this work with
inlineStyleThresholdis to lower the value to 2048.The component in question:
<svelte:element this={tag} {href} type={tag === 'button' ? type : undefined} class={['button text-caps', className]} class:is-active={isActive} class:is-inactive={isInactive} disabled={tag === 'button' ? disabled : null} tabindex={tag === 'button' ? 0 : null} target={isExternal ? '_blank' : undefined} rel={isExternal ? 'noreferrer noopener' : undefined} data-sveltekit-noscroll={!href ? undefined : href && !isExternal ? '' : undefined} role={tag === 'button' ? 'button' : undefined} {onclick}
{@render children?.()} {#if label} <span class="label" data-label={label}> <span>{label}</span> </span> {/if} {#if withIcon && icon} <span class="icon">{icon}</span> {/if}</svelte:element>
Could it be because of the
<svelte:element>?EDIT: having refactored the component to use a
content()snippet and conditions based on the tag, it's not that!
I can't reproduce this in https://stackblitz.com/github/eltigerchino/13878
If both of you really can't create a minimal reproduction, if you're open to it, you could temporarily share the private repository with me and I can give it a try too.