Skip to content

hidden="until-found" is rendered as just hidden when passed via dynamic attributes #15521

@ddryo

Description

@ddryo

Astro Info

Astro v5.11.1 (also confirmed in latest main branch source)

Describe the Bug

The hidden attribute is listed in htmlBooleanAttributes in packages/astro/src/runtime/server/render/util.ts, so any truthy value is rendered as just hidden without preserving the actual string value.

This means hidden="until-found" (a valid HTML enumerated attribute value) cannot be set via dynamic attributes or spread syntax.

Static HTML in .astro templates preserves the value correctly:

<!-- ✅ Output: <div hidden="until-found"> -->
<div hidden="until-found">content</div>

Dynamic attributes lose the value:

---
const props = { hidden: "until-found" };
---

<!-- ❌ Output: <div hidden> -->
<div {...props}>content</div>

<!-- ❌ Output: <div hidden> -->
<div hidden={"until-found"}>content</div>

This also affects any Astro component that internally spreads props onto HTML elements — there is no way to pass hidden="until-found" through a component.

Root Cause

In packages/astro/src/runtime/server/render/util.ts:

if (htmlBooleanAttributes.test(key)) {
    return markHTMLString(value ? ` ${key}` : '');
}

The hidden attribute is treated as purely boolean. However, the HTML spec defines hidden as an enumerated attribute that accepts:

  • "" or "hidden" — behaves like the boolean hidden
  • "until-found" — hides the element but allows browser find-in-page and fragment navigation to reveal it

What's the expected result?

When hidden has the string value "until-found", it should be rendered as hidden="until-found" instead of just hidden.

A possible fix would be to special-case hidden in the boolean attribute handler:

if (htmlBooleanAttributes.test(key)) {
    if (key === 'hidden' && value === 'until-found') {
        return markHTMLString(` ${key}="${value}"`);
    }
    return markHTMLString(value ? ` ${key}` : '');
}

Or more generally, allow string values to pass through for boolean attributes when the value is not simply true:

if (htmlBooleanAttributes.test(key)) {
    if (typeof value === 'string' && value !== '' && value !== 'true') {
        return markHTMLString(` ${key}="${escapeHTML(value)}"`);
    }
    return markHTMLString(value ? ` ${key}` : '');
}

Impact

hidden="until-found" is used for:

  • Accordion/disclosure patterns where browser find-in-page should search collapsed content
  • SEO — search engines can index content inside hidden="until-found" elements
  • Accessibility — beforematch event enables progressive disclosure

Without this fix, component libraries that wrap HTML elements (using spread) cannot use this feature.

Note: React had the same issue and fixed it in React 19.

Participation

  • I am willing to submit a pull request for this issue.

Metadata

Metadata

Assignees

No one assigned

    Labels

    - P3: minor bugAn edge case that only affects very specific usage (priority)pkg: astroRelated to the core `astro` package (scope)

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions