The as Prop
Used to change the underlying HTML element that a component renders. It provides a straightforward way to change the underlying element while retaining the component’s functionality.
<Heading as="h3">Hello, world!</Heading>TypeScript: the caveat with the
asprop is that the type of the component passed to it must be compatible with the component’s props. We do not infer the underlying component’s props fromas.
as stays a loose ValidComponent rather than a generic that re-types props from the element, and
that is deliberate rather than unfinished: the deep conditional types that would buy you href on
as="a" also wreck editor completions across every component in the library.
The render Prop
Used to compose a component’s functionality onto an element you supply — the React version’s
asChild, which this library does not have.
It is a function that receives the computed props and returns the element, never a JSX element.
A Solid JSX element is an already-constructed node by the time it reaches us and there is no
cloneElement, so accepting one could only mean rendering it with every computed prop dropped.
<Button render={(props) => <MyButton {...props}>Open</MyButton>} />That cast is real, and it is narrow. Every component types its element props against the element
it renders — Box’s are a div’s, Span’s a span’s — and Solid’s Ref<HTMLDivElement> is not
assignable to Ref<HTMLAnchorElement>, so a render target that is a different host element
needs a cast. A target that renders the same element needs none, and neither does a component
typed against the same JSX.HTMLAttributes. A component whose props are a Chakra type needs one
for a different reason, which Best Practices below spells out.
Merging the Computed Class
render hands your element the computed props and steps back. It does not reconcile them with what
you write beside the spread — a JSX spread is last-wins, and class is a prop like any other:
// your class wins — the computed class is lost
<Foo {...props} class="my-icon" />
// the computed class wins — yours is lost
<Foo class="my-icon" {...props} />Merging the two is yours to spell, with Panda’s own cx:
import { cx } from "./styled-system/css";
<Button
render={(props) => (
<MyButton {...props} class={cx(props.class, "my-button")} />
)}
/>;You already have cx. Running Panda in your build is this library’s hard prerequisite — it is the
only thing that turns a recipe or a style prop into CSS at all — so ./styled-system/css is
generated in your project whether you have imported from it before or not.
The library cannot do the merge for you. It knows the string it computed and nothing about yours: whether it is meant to layer over the recipe, to replace it, or to belong to some other element in the tree you returned. Only the call site sees both, so only the call site can decide.
That is deliberately more capable than the React version’s asChild, which clones your element
and always concatenates the two — so a consumer who wants to replace the computed class has no way
to say so. A function is handed both strings and picks: stack them with cx, keep only yours, or
keep only the computed one.
Getting it wrong is quiet. The computed class is where the component’s recipe lives, and an element whose class names no rule renders exactly as it would with the right one — nothing throws, nothing warns, nothing is missing from the tree. There is no runtime stylesheet to catch it either, because this library never writes one. So when a composed element comes out looking unstyled, the class attribute is the first thing to read.
The unstyled Prop
Used to drop a component’s own theme styles while keeping everything else it does. Style props and
the css prop still apply — the opt-out is of the recipe, not of styling.
<Checkmark unstyled css={{ color: "fg.inverted" }} />This is how one component re-dresses another it is built from, rather than a way to render a bare
element: a checkbox built on Checkmark renders <Checkmark unstyled> and hands it its own slot’s
styles, so the mark takes the checkbox’s size and colour instead of the checkmark recipe’s.
Best Practices
as and render both hand your element every prop the component computed for it — the composed
class, the merged ref, the children, and the DOM attributes. To avoid common pitfalls, there are
a few practices to consider:
- Spread the props. A target that picks them off by name loses whichever it did not name — the
class, theref, an attribute — and nothing errors. - Never destructure them. They are a live props proxy whose
classis a getter that recomputes when a style prop changes, sorender={({ class: className }) => …}reads it once and freezes the element at whatever it was on the first render. - Refs need no forwarding. Solid has no
forwardRef:refarrives as an ordinary prop, and the spread applies it whether the target is a host element or another chakra-ui-solid component. Dropping the spread is the only way to lose it.
const MyButton = (props: ComponentProps<"button">) => <button {...props} />;
// with `as` — Button renders MyButton in place of its own element
<Button as={MyButton}>Click me</Button>
// with `render` — the same target, called with the computed props
<Button render={(props) => <MyButton {...props}>Click me</MyButton>} />Type the target against the element’s props, as MyButton does, rather than against the Chakra
component’s. What a target is handed is the DOM props, and a Chakra props type spells five of those
attributes htmlSize, htmlWidth, htmlHeight, htmlTranslate and htmlContent — keeping the
plain names for the style props of the same name — so a target typed BoxProps or ButtonProps
does not accept them without a cast.