What
@stacksjs/components's <Notification> is the package's only toast/alert primitive, but it's authored as a <script server> SFC. Its show prop is captured at SSG/SSR time via @if(show), so consumers can't drive it from client-side signals — the typical "fetch returns → toast appears" flow.
Repro
In any consuming app (e.g. a coming-soon page wired to a /api/email/subscribe POST), bind reactive client signals to the component:
<script client>
const toastVisible = state(false)
const toastTitle = state('')
const toastMessage = state('')
const toastType = state('success')
async function submitEmail() {
const res = await fetch('/api/email/subscribe', { method: 'POST', /* ... */ })
if (res.ok) {
toastTitle.set('Thanks!')
toastMessage.set('Stay tuned for more updates.')
toastType.set('success')
toastVisible.set(true)
}
}
</script>
<Notification
:show="toastVisible()"
:title="toastTitle()"
:message="toastMessage()"
:type="toastType()"
position="top-right"
/>
What happens
- The
Notification.stx <script server> block evaluates once at render time, so $props.show, $props.title, etc. all resolve to their initial (empty) values.
- The rendered HTML inlines the package's markup with
bg-blue-50 (the default info type) and empty title/message paragraphs.
toastVisible.set(true) later in submitEmail() doesn't change anything in the DOM — the :show binding is left as a stray attribute on the rendered <div>, never re-evaluated.
- Inspecting the SSG output:
<div :title="toastTitle()" :message="toastMessage()" :type="toastType()"
class="fixed z-50 m-4 max-w-md ... bg-blue-50 text-blue-900 ..."
role="alert">
<div class="flex items-start">
<div class="flex-1"></div> <!-- empty: title/message captured as '' at SSG time -->
...
</div>
</div>
Expected
A toast/notification primitive in a UI component library is almost always driven by client state (button click → show → auto-dismiss). The current <Notification> only fits the static-flash-message case ("on this page render, was the form submitted? Yes → render the green box once"). For the dynamic case, consumers have to either:
- Copy
Notification.stx locally and rewrite <script server> → <script client>.
- Hand-roll a bespoke toast and hope they replicate the package's design tokens correctly.
- Use the package's CSS classes (
bg-green-50, text-green-900, etc.) on a custom client-rendered element — works only if the Notification.stx source is in the consumer's class-extraction path so Crosswind generates the utilities.
None of those are obvious from the README, and (3) requires understanding the Crosswind extraction model.
Proposal
Convert the <Notification> SFC from <script server> to <script client> so its props can be reactively bound. Concretely:
- Move prop derivation (
show, title, message, type, position, duration, onClose, className) to client-side state()/derived() (or accept signals directly and read with ()).
- Replace
@if(show) with :if="show()" (or always render and toggle visibility via x-class/data-* attributes for nicer transitions).
- Replace
{{ title }} / {{ message }} with :text="title()" / :text="message()".
- Leave the existing
setTimeout auto-hide block, but wire it through the client signal.
If a static/SSR usage is still wanted, expose a sibling <NotificationStatic> (or accept a static prop that opts out of the signal layer) so apps that just want a one-shot flash message keep working.
Happy to PR this if there's a preferred direction — the current behaviour was a real footgun while wiring up a notify-me toast in a downstream app.
Repo / version
@stacksjs/components@0.2.35
@stacksjs/stx@0.2.52
- bun
1.3.13
What
@stacksjs/components's<Notification>is the package's only toast/alert primitive, but it's authored as a<script server>SFC. Itsshowprop is captured at SSG/SSR time via@if(show), so consumers can't drive it from client-side signals — the typical "fetch returns → toast appears" flow.Repro
In any consuming app (e.g. a coming-soon page wired to a
/api/email/subscribePOST), bind reactive client signals to the component:What happens
Notification.stx<script server>block evaluates once at render time, so$props.show,$props.title, etc. all resolve to their initial (empty) values.bg-blue-50(the defaultinfotype) and empty title/message paragraphs.toastVisible.set(true)later insubmitEmail()doesn't change anything in the DOM — the:showbinding is left as a stray attribute on the rendered<div>, never re-evaluated.Expected
A toast/notification primitive in a UI component library is almost always driven by client state (button click → show → auto-dismiss). The current
<Notification>only fits the static-flash-message case ("on this page render, was the form submitted? Yes → render the green box once"). For the dynamic case, consumers have to either:Notification.stxlocally and rewrite<script server>→<script client>.bg-green-50,text-green-900, etc.) on a custom client-rendered element — works only if theNotification.stxsource is in the consumer's class-extraction path so Crosswind generates the utilities.None of those are obvious from the README, and (3) requires understanding the Crosswind extraction model.
Proposal
Convert the
<Notification>SFC from<script server>to<script client>so its props can be reactively bound. Concretely:show,title,message,type,position,duration,onClose,className) to client-sidestate()/derived()(or accept signals directly and read with()).@if(show)with:if="show()"(or always render and toggle visibility viax-class/data-*attributes for nicer transitions).{{ title }}/{{ message }}with:text="title()"/:text="message()".setTimeoutauto-hide block, but wire it through the client signal.If a static/SSR usage is still wanted, expose a sibling
<NotificationStatic>(or accept astaticprop that opts out of the signal layer) so apps that just want a one-shot flash message keep working.Happy to PR this if there's a preferred direction — the current behaviour was a real footgun while wiring up a notify-me toast in a downstream app.
Repo / version
@stacksjs/components@0.2.35@stacksjs/stx@0.2.521.3.13