hope-uiearly-preview

Polymorphism

Render any hope-ui component as a different element or component with the render prop — there is no as prop.

The render prop

Every hope-ui component has a single polymorphism API: the render prop. It's a function that receives the component's fully-computed props and spreads them onto the element you return.

<Badge render={(props) => <a href="/tags/new" {...props} />}>Linkable</Badge>

render is a function, never an element — and there is deliberately no as prop. An element form could only ever drop the component's computed props, so it would silently render something unstyled and inert. A function forces you to spread them, so the styling and behavior always reach your element.

The most common use is turning a component into a link while keeping its look. Because Badge is a static label with no behavior of its own, an anchor is all you need — the anchor brings its own link semantics, and Badge just contributes its computed class and merges refs.

Badge as an anchor
<Badge
  variant="soft"
  colorScheme="primary"
  render={(props) => <a href="/tags/new" {...props} />}
>
  Linkable tag
</Badge>

Preserve behavior with an interactive component

An interactive component is different. A native <button> carries behavior an <a> doesn't — role, tab order, and Space/Enter activation. When you render Button as anything other than a button, set nativeButton={false} so Button re-supplies that behavior itself: role="button", tab-order management, and synthesized keyboard activation.

Button as an anchor
<Button
  variant="solid"
  colorScheme="primary"
  nativeButton={false}
  render={(props) => <a href="/docs" {...props} />}
>
  Link that acts as a button
</Button>

Render as a component

render also targets a component, not just host elements — for example your router's Link. hope-ui merges its own internal ref with the rendered element's into a single function ref, which a router Link honors, so you get hope-ui's styling and press behavior together with real client-side navigation.

Render as a router Link
import type { ComponentProps } from "solid-js";
import { Link } from "@tanstack/solid-router";
import { Button } from "@hope-ui/components/button";
 
<Button
  variant="solid"
  colorScheme="primary"
  nativeButton={false}
  render={(props) => (
    <Link {...(props as unknown as ComponentProps<typeof Link>)} to="/" />
  )}
>
  Back to home
</Button>;

Ref merging

You never wire up refs yourself. render collapses the component's internal ref and any ref you pass into one function ref (it delegates to @solidjs/web's applyRef, which flattens arrays and skips falsy values). There is no mergeRefs helper to reach for — merging is automatic, and it works with any render target.

let anchor: HTMLAnchorElement | undefined;
 
<Button
  nativeButton={false}
  ref={anchor}
  render={(props) => <a href="/docs" {...props} />}
>
  Both refs resolve to the same anchor
</Button>;

Typing

Each component types its render against its own element's attributes — Button's render receives button attributes, Badge's receives span attributes. Rendering as a different element (button → anchor) type-checks once you assert the props at the spread:

<Button
  nativeButton={false}
  render={(props) => (
    <a
      {...(props as unknown as JSX.AnchorHTMLAttributes<HTMLAnchorElement>)}
      href="/docs"
    />
  )}
>
  Docs
</Button>

This is a deliberate trade-off: hope-ui avoids a Polymorphic<T> generic — and the IntelliSense cost that comes with it — so a cross-element render needs one explicit cast. The runtime behavior is unaffected.

At a glance

PropDefaultType
render
(props) => JSX.ElementRender the component as another element or component. Receives the computed props to spread. The only polymorphism mechanism — there is no as prop.
nativeButton
booleanOn interactive components like Button: set false when rendering a non-button so the component supplies role, tabIndex, and keyboard activation. Static components like Badge don't need it.