leptos-rs/leptos · error
there to be a <html> element
Error message
there to be a <html> element
What it means
leptos_meta's <Html> component builds its view state by calling document().document_element() and unwrapping it, since the component only makes sense when rendering the root <html> element. If the document has no root element (i.e. we are not rendering into a real browser document, or the Html component was mounted somewhere other than a document root), the expect panics with this message. The library throws it because <Html> attributes (class, lang, etc.) can only be applied to the single <html> root.
Source
Thrown at meta/src/html.rs:69
}
struct HtmlViewState<At>
where
At: Attribute,
{
attributes: At::State,
}
impl<At> Render for HtmlView<At>
where
At: Attribute,
{
type State = HtmlViewState<At>;
fn build(self) -> Self::State {
let el = document()
.document_element()
.expect("there to be a <html> element");
let attributes = self.attributes.build(&el);
HtmlViewState { attributes }
}
fn rebuild(self, state: &mut Self::State) {
self.attributes.rebuild(&mut state.attributes);
}
}
impl<At> AddAnyAttr for HtmlView<At>
where
At: Attribute,
{
type Output<SomeNewAttr: Attribute> =
HtmlView<<At as NextAttribute>::Output<SomeNewAttr>>;
View on GitHub (pinned to 32d20f6c9d)
Solutions
- Ensure the server-rendered template includes a real <html>...</html> root element so document_element() is populated on hydration.
- Only render <Html> at the very root of the app (inside the hydration mount), not inside nested views or detached fragments.
- Verify hydration is running in a browser (or browser-like test harness such as wasm-bindgen-test with a DOM), not in a headless JS-only environment without a document.
- Check custom mount logic: if you replace document contents manually, keep the <html> element attached to the document.
Example fix
// before: rendering Html inside a nested, detached view
view! { <div><Html lang="en"/></div> }
// after: Html at the document root
view! { <Html lang="en"/> <App routes/> } Defensive patterns
Strategy: validation
Validate before calling
let has_root = web_sys::window()
.and_then(|w| w.document())
.and_then(|d| d.document_element())
.is_some();
if !has_root { console_error!("<Html> requires a document with <html> root"); return; } Type guard
fn has_document_root() -> bool {
web_sys::window().map(|w| w.document()).flatten()
.and_then(|d| d.document_element()).is_some()
} Prevention
- Render <Html> only at the app root inside a full browser document.
- Keep <html>...</html> in the SSR shell intact.
- Run hydration only in browser or DOM-equipped test environments.
When it happens
Trigger: Mounting the <Html> component (or a component that uses it, like <MetaProvider>/routes with html attribute props) during client-side build/hydrate when document().document_element() returns None — e.g. running hydration code outside a browser environment, or the mount target replaced/removed the <html> element.
Common situations: Running WASM hydration in a non-browser context (tests without DOM, ssr-only code accidentally hydrated), mounting a Leptos app into a fragment detached from the document, or a custom root template missing <html>.
Related errors
- there to be a <body> element
- missing <head> element
- lazy routes should not be used with hydrate_body(); use hydr
- Expected a [number, string] tuple
- failed to read hash file
AI-assisted analysis of leptos-rs/leptos@32d20f6c9d (2026-09-01).
Data as JSON: /api/errors/da7777aef6e5b6ef.
Report an issue: GitHub.