Skip to main contentSkip to docs navigation

TypeScript

How component prop types are exported for reuse, and how some components accept a component prop that keeps type-checking after swapping the rendered element.

@chassis-ui/react is written in TypeScript, and every component's prop type is exported alongside the component itself — there's no separate @types package and nothing to install.

Prop types

Import a component's prop type directly from @chassis-ui/react to type a wrapper around it, or a forwardRef-based custom component that forwards the same props:

TSX
import { Button, ButtonProps } from '@chassis-ui/react'

function SubmitButton(props: ButtonProps) {
  return <Button type="submit" {...props} />
}

Responsive values

A layout prop that can change with the width, such as direction of Flex or span of GridItem, is typed Responsive<T>: the value itself, or an object with a value per breakpoint, where base is the value from the narrowest width. Import the type to pass such a prop through a component of your own:

TSX
import { GridItem, Responsive } from '@chassis-ui/react'

function Field({ span = { base: 'full', md: 6 } }: { span?: Responsive<number | 'full'> }) {
  return <GridItem span={span} />
}

The second type argument is the keys the object reads, the viewport breakpoints by default. Grid and GridItem also read the container keys, so their props are Responsive<T, Breakpoint | ContainerBreakpoint>.

The component prop

A handful of components — Placeholder, CardImage, Skeleton, SkeletonLoader, and MenuToggle — accept a component prop that swaps the element rendered at their root while keeping the rest of that element's own props type-checked at the call site. Passing a framework's own Image component, for example, exposes its fill/priority/loader props with full autocomplete, on top of the props Placeholder already defines:

TSX
import Image from 'next/image'
import { Placeholder } from '@chassis-ui/react'

<Placeholder component={Image} src="/hero.jpg" width={800} height={400} alt="" fill priority />

The asChild prop

Every component that accepts component also accepts asChild, which is the other way to change what gets rendered. Instead of naming a tag or component for the component to render, you render the element yourself as the only child, and the component merges its classes, props and ref onto it:

TSX
import Link from 'next/link'
import { Button, NavLink } from '@chassis-ui/react'

<Button asChild>
  <Link href="/login">Log in</Link>
</Button>

<NavLink asChild active>
  <Link href="/">Home</Link>
</NavLink>

The child's own props win over the component's, except that class names are combined and event handlers are chained (the component's runs first). Use asChild when passing from a React Server Component, where component={Link} fails to build, and whenever you'd rather write the target element as ordinary JSX. SkeletonLoader is the one exception: it renders no element of its own, so it has no asChild.

What the child element gets

A component treats its child as it treats the same element passed to component. <Button asChild disabled><a href="/login"> renders what <Button component="a" href="/login" disabled> renders. What that is depends on the child:

ChildTreated asWhen the component is disabled
<a>, or a component element with href or toa linkaria-disabled="true", tabindex="-1", and the click is blocked
<button>, <input>the native controlthe disabled attribute
Any other tag, such as <div>a plain elementas component="div": Button and CloseButton add role="button" and keyboard activation
A component element with neither href nor toa componentdisabled is passed to it as a prop

A router link such as <Link href="/login"> of Next.js or <Link to="/login"> of React Router counts as a link because of its href or to: it renders an <a> and forwards what it's given, so it needs the link's handling rather than a disabled attribute. When a link is disabled, the child's own onClick doesn't run either. The same holds for a router link passed as component: <Button component={Link} href="/login" disabled> is treated as a link too.

By family:

  • Button, Chip, PaginationItem, Avatar apply the table above.

  • Link, NavLink, CardLink, MenuItem do the same; MenuItem keeps role="menuitem" on the child.

  • CloseButton gives a link, a <button> or a plain tag its close-button classes and the Close label. A component child with no href or to, such as <Button>, keeps its own look, as it does with component={Button}.

  • ListItem, StepperItem: a link or <button> child makes the item an action (list-action on a ListItem), and List or Stepper renders a <div> instead of a <ul>/<ol> around it, as for component="a".

  • Placeholder renders the child as the image, with fluid, rounded, thumbnail and align applied. The child carries its own src:

    TSX
    import Image from 'next/image'
    
    <Placeholder asChild fluid rounded>
      <Image src="/hero.jpg" width={800} height={400} alt="" />
    </Placeholder>
  • Every other component has no element-specific behavior: its classes, props and ref go onto the child.

Supported components

Every component listed above uses the same generic pattern, typed against ElementType — passing a string renders that HTML tag with its native attributes typed, passing a component renders it with its own declared props typed. Link's component prop is narrower by comparison: it swaps the rendered tag or component (e.g. a router's own Link) without also changing which props are accepted, since Link's own prop set already covers the common case.

Typing a custom target

Passing a custom component to component still enforces that component's own required props — omitting one is a type error at the call site, the same as calling that component directly:

TSX
function BrandedImage(props: { src: string; alt: string; caption: string }) {
  return (
    <figure>
      <img src={props.src} alt={props.alt} />
      <figcaption>{props.caption}</figcaption>
    </figure>
  )
}

// Error: Property 'caption' is missing
<Placeholder component={BrandedImage} src="/hero.jpg" alt="" />