ReviewOS

stacks/stx

Drawer: slide-in keyframe is never interpolated, and a multi-word title breaks aria-label

#1982
Closed glennmichael123 opened this 9 days ago ·
9 days ago

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=&quot;Workout details&quot;

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&quot;', 'tabindex']   ← 'details&quot;' is now an attribute

So 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/components 0.2.339, stacks 0.75.31, Bun 1.3.x, macOS
  • Found while migrating hq.training onto the component library.

Sign in to comment on this issue.