Two independent rendering defects in Drawer, found while adopting it. Both are silent.
1. The slide-in animation never runs
Drawer.stx ends with:
<style>
@keyframes slideIn {
from {
transform: {{ position === 'right' ? 'translateX(100%)' : position === 'left' ? 'translateX(-100%)' : … }};
}
to { transform: translate(0, 0); }
}
</style>A component's <style> element is lifted out of the source before expressions run, so that mustache is never evaluated. What ships is:
@keyframes slideIn {
from {
transform: {{ position === 'right' ? 'translateX(100%)' : … }};
}The declaration is invalid, so the browser discards it. Read back from the CSSOM:
@keyframes slideIn {
0% { } // ← empty
100% { transform: translate(0px, 0px); }
}The from frame is gone, so the panel does not slide in from its edge — it appears in place. fadeIn, in the same block, is correct, because it has nothing to interpolate.
This is the constraint the component docs already describe (<style></style> ships as <style></style>); Drawer is a component that ran into it. The fix is to move the offset off the keyframe: four static @keyframes (one per position) selected by class, or the existing inline style attribute carrying a custom property the keyframe reads.
Worth considering more generally: an uninterpolated {{ reaching a <style> block is always a bug, and a build-time check for it would have caught this one.
2. A two-word title breaks the accessible name and injects a junk attribute
{{ title ? 'aria-label="' + title + '"' : '' }}{{ }} HTML-escapes its result, so the quotes this builds are escaped after they were meant to act as attribute syntax. With title="Workout details" the server emits:
aria-label="Workout details"and the parser produces two attributes:
p.getAttribute('aria-label') // '"Workout' ← leading quote, truncated at the space
[...p.attributes].map(a => a.name)
// [… 'aria-label', 'details"', 'tabindex'] ← 'details"' is now an attributeSo every drawer with a multi-word title has the wrong accessible name — the one thing the title prop is there to set.
(The quotes are escaped, so this is a correctness and a11y bug rather than an injection.)
The fix is to bind the attribute rather than build it as a string — :aria-label="title", or emit aria-label="{{ title }}" unconditionally and let an empty title be an empty label.
Why this one matters
Drawer is the most complete component in the package — it locks body scroll, moves focus in and back out, scopes its focus lookup to its own $uid, and binds Escape on the document precisely because the panel handler is unreachable before focus lands. It is the component I would hold the others to. These two defects are both in its presentation layer and are worth clearing so that stays true.
Environment
@stacksjs/components0.2.339, stacks 0.75.31, Bun 1.3.x, macOS- Found while migrating hq.training onto the component library.