T
TanStack2y ago
passive-yellow

Using Tailwind's Catalyst UI with Tanstack Router

Hello! Catalyst requires a bit of setup with the client-side router (https://catalyst.tailwindui.com/docs#client-side-router-integration) and attempting to strictly type the Link component. I pulled heavily from @Manuel Schiller's example from another thread (https://discord.com/channels/719702312431386674/1192472012585705504/1193343346215755828) and the below technically "works" but TS is stating that the type is wrong. Feel like I'm really close but overlooking something as I'm definitely out of my league here. Appreciate any help here and will create an issue in Tailwind's issues repo with the solution to add it to their docs!
import { DataInteractive as HeadlessDataInteractive } from '@headlessui/react'
import React from 'react'
import { Link as TanstackLink, AnyRoute, RegisteredRouter, RoutePaths, LinkProps } from '@tanstack/react-router'

export const Link = React.forwardRef(function Link<
TRouteTree extends AnyRoute = RegisteredRouter['routeTree'],
TFrom extends RoutePaths<TRouteTree> | string = string,
TTo extends string = '',
TMaskFrom extends RoutePaths<TRouteTree> | string = TFrom,
TMaskTo extends string = ''
>(
props: { href: string | LinkProps<TRouteTree, TFrom, TTo, TMaskFrom, TMaskTo>["to"] } & Omit<LinkProps<TRouteTree, TFrom, TTo, TMaskFrom, TMaskTo>, "to">,
ref: React.ForwardedRef<HTMLAnchorElement>
) {
return (
<HeadlessDataInteractive>
<TanstackLink {...props} to={props.href} ref={ref} />
</HeadlessDataInteractive>
)
})
import { DataInteractive as HeadlessDataInteractive } from '@headlessui/react'
import React from 'react'
import { Link as TanstackLink, AnyRoute, RegisteredRouter, RoutePaths, LinkProps } from '@tanstack/react-router'

export const Link = React.forwardRef(function Link<
TRouteTree extends AnyRoute = RegisteredRouter['routeTree'],
TFrom extends RoutePaths<TRouteTree> | string = string,
TTo extends string = '',
TMaskFrom extends RoutePaths<TRouteTree> | string = TFrom,
TMaskTo extends string = ''
>(
props: { href: string | LinkProps<TRouteTree, TFrom, TTo, TMaskFrom, TMaskTo>["to"] } & Omit<LinkProps<TRouteTree, TFrom, TTo, TMaskFrom, TMaskTo>, "to">,
ref: React.ForwardedRef<HTMLAnchorElement>
) {
return (
<HeadlessDataInteractive>
<TanstackLink {...props} to={props.href} ref={ref} />
</HeadlessDataInteractive>
)
})
Getting started - Catalyst UI Kit
Modern application UI components to kickstart your design system.
10 Replies
absent-sapphire
absent-sapphire2y ago
can you provide a minimal example on e.g. codesandbox?
absent-sapphire
absent-sapphire2y ago
i would just remove the & Omit<LinkProps<TRouteTree, TFrom, TTo, TMaskFrom, TMaskTo>, "to">, https://codesandbox.io/p/devbox/catalyst-tanstack-router-forked-qv78hr?file=%2Fsrc%2Flink.tsx%3A34%2C27
passive-yellow
passive-yellowOP2y ago
Tried that but breaks any component that utilizes the underlying Link component, updated the codesandbox with a sample component mirroring the TextLink component to give an example of it in use
passive-yellow
passive-yellowOP2y ago
Thanks for the help! I'll work on updating the other components but this was extremely helpful!
absent-sapphire
absent-sapphire2y ago
@B0NECH0P I were coming across the same thing a few days ago, this was my solution. Looks similar to yours, although I didn't passthrough all the generics.
import { DataInteractive as HeadlessDataInteractive } from '@headlessui/react'
import { Link as RouterLink, LinkProps } from '@tanstack/react-router'
import React from 'react'

export const Link = React.forwardRef(function Link(
props: { href: string | LinkProps['to'] } & Omit<LinkProps, 'to'>,
ref: React.ForwardedRef<HTMLAnchorElement>,
) {
return (
<HeadlessDataInteractive>
<RouterLink {...props} ref={ref} />
</HeadlessDataInteractive>
)
})
import { DataInteractive as HeadlessDataInteractive } from '@headlessui/react'
import { Link as RouterLink, LinkProps } from '@tanstack/react-router'
import React from 'react'

export const Link = React.forwardRef(function Link(
props: { href: string | LinkProps['to'] } & Omit<LinkProps, 'to'>,
ref: React.ForwardedRef<HTMLAnchorElement>,
) {
return (
<HeadlessDataInteractive>
<RouterLink {...props} ref={ref} />
</HeadlessDataInteractive>
)
})
Edit: Don't use this.. you don't get type safety for link props anymore... I was struggeling with this once more today. My solution without forwarding the generics doesn't provide type safety in the catalyst components. But I also didn't got the solution from codesandbox above to work. One additional problem occurred: Typesafety for 'props' does only work if there is a matching 'to'. Rematching 'to' to 'href' because Catalyst checks for the existence of 'href' in multiple places does make the 'props' not typesafe anymore. But finally I found something which works for me and is - as far as I can tell now - fully typesafe:
import { routeTree } from '@/routeTree.gen'
import { DataInteractive as HeadlessDataInteractive } from '@headlessui/react'
import { Link as RouterLink, LinkProps } from '@tanstack/react-router'
import React from 'react'

export const Link = React.forwardRef(function Link(
// Providing the actual routeTree here
props: LinkProps<typeof routeTree>,
ref: React.ForwardedRef<HTMLAnchorElement>,
) {
return (
<HeadlessDataInteractive>
<RouterLink {...props} ref={ref} />
</HeadlessDataInteractive>
)
})
import { routeTree } from '@/routeTree.gen'
import { DataInteractive as HeadlessDataInteractive } from '@headlessui/react'
import { Link as RouterLink, LinkProps } from '@tanstack/react-router'
import React from 'react'

export const Link = React.forwardRef(function Link(
// Providing the actual routeTree here
props: LinkProps<typeof routeTree>,
ref: React.ForwardedRef<HTMLAnchorElement>,
) {
return (
<HeadlessDataInteractive>
<RouterLink {...props} ref={ref} />
</HeadlessDataInteractive>
)
})
The only downside of this is that I have to change all other Catalyst components mentioning 'Link' and replace 'href' by 'to'. One more update: Changing all other Catalyst components would be quite complicated, so I - for now - ended with this: The prop is called 'href' but it accepts an object 'ToOptions'. Downside now is that typescript does not warn for missing keys in params, but I am accepting that for now...
import { routeTree } from '@/routeTree.gen'
import { DataInteractive as HeadlessDataInteractive } from '@headlessui/react'
import {
Link as RouterLink,
LinkProps,
ToOptions,
} from '@tanstack/react-router'
import React from 'react'

export const Link = React.forwardRef(function Link(
{
href: toOptions,
...props
}: { href: ToOptions<typeof routeTree> } & Omit<
LinkProps<typeof routeTree>,
keyof ToOptions<typeof routeTree> | 'href'
>,
ref: React.ForwardedRef<HTMLAnchorElement>,
) {
return (
<HeadlessDataInteractive>
<RouterLink {...props} {...toOptions} ref={ref} />
</HeadlessDataInteractive>
)
})
import { routeTree } from '@/routeTree.gen'
import { DataInteractive as HeadlessDataInteractive } from '@headlessui/react'
import {
Link as RouterLink,
LinkProps,
ToOptions,
} from '@tanstack/react-router'
import React from 'react'

export const Link = React.forwardRef(function Link(
{
href: toOptions,
...props
}: { href: ToOptions<typeof routeTree> } & Omit<
LinkProps<typeof routeTree>,
keyof ToOptions<typeof routeTree> | 'href'
>,
ref: React.ForwardedRef<HTMLAnchorElement>,
) {
return (
<HeadlessDataInteractive>
<RouterLink {...props} {...toOptions} ref={ref} />
</HeadlessDataInteractive>
)
})
wise-white
wise-white13mo ago
TS2322: Type
{ ref: ForwardedRef<HTMLAnchorElement>; to?: '' | './' | '../' | undefined; hash?: true | Updater<string> | undefined; state?: true | NonNullableUpdater<...> | undefined; ... 17 more ...; children?: ReactNode | ((state: { ...; }) => ReactNode); }
is not assignable to type
ToSubOptionsProps<Router<RootRoute<{}, {}, {}, RouteContext, RouteContext, {}, {}, {}, {}, { readonly IndexRoute: Route<RootRoute<{}, {}, {}, RouteContext, RouteContext, ... 4 more ..., unknown>, ... 17 more ..., unknown>; readonly authSignInRoute: Route<...>; }>, TrailingSlashOption, Record<...>, Record<...>>, stri...
Types of property to are incompatible.
Type '' | './' | '../' | undefined is not assignable to type
'/' | '/sign-in' | './' | '../' | './sign-in' | '../../' | undefined
Type '' is not assignable to type
'/' | '/sign-in' | './' | '../' | './sign-in' | '../../' | undefined
TS2322: Type
{ ref: ForwardedRef<HTMLAnchorElement>; to?: '' | './' | '../' | undefined; hash?: true | Updater<string> | undefined; state?: true | NonNullableUpdater<...> | undefined; ... 17 more ...; children?: ReactNode | ((state: { ...; }) => ReactNode); }
is not assignable to type
ToSubOptionsProps<Router<RootRoute<{}, {}, {}, RouteContext, RouteContext, {}, {}, {}, {}, { readonly IndexRoute: Route<RootRoute<{}, {}, {}, RouteContext, RouteContext, ... 4 more ..., unknown>, ... 17 more ..., unknown>; readonly authSignInRoute: Route<...>; }>, TrailingSlashOption, Record<...>, Record<...>>, stri...
Types of property to are incompatible.
Type '' | './' | '../' | undefined is not assignable to type
'/' | '/sign-in' | './' | '../' | './sign-in' | '../../' | undefined
Type '' is not assignable to type
'/' | '/sign-in' | './' | '../' | './sign-in' | '../../' | undefined
I get this error on RouterLink while using this. 🤔
passive-yellow
passive-yellowOP13mo ago
Saw your DM @Baqir, here's what I ended up with: In Catalyst's Link component:
import { router } from "@/." // Exported router from `createRouter`
import * as Headless from '@headlessui/react'
import React, { forwardRef } from 'react'
import { LinkProps, Link as RouterLink } from "@tanstack/react-router"

export const Link = forwardRef(function Link(
props: { href?: LinkProps<typeof router>; className?: string | undefined; tabIndex?: number | undefined } & Omit<LinkProps<typeof router>, "to">,
ref: React.ForwardedRef<HTMLAnchorElement>
) {
const newProps = {...props, href: undefined}

return (
<Headless.DataInteractive>
<RouterLink {...props.href} {...newProps} ref={ref} />
</Headless.DataInteractive>
)
})
import { router } from "@/." // Exported router from `createRouter`
import * as Headless from '@headlessui/react'
import React, { forwardRef } from 'react'
import { LinkProps, Link as RouterLink } from "@tanstack/react-router"

export const Link = forwardRef(function Link(
props: { href?: LinkProps<typeof router>; className?: string | undefined; tabIndex?: number | undefined } & Omit<LinkProps<typeof router>, "to">,
ref: React.ForwardedRef<HTMLAnchorElement>
) {
const newProps = {...props, href: undefined}

return (
<Headless.DataInteractive>
<RouterLink {...props.href} {...newProps} ref={ref} />
</Headless.DataInteractive>
)
})
There are a few Catalyst components that you need to update the href properties from strings to LinkProps from @tanstack/react-router but works well and still maintaining some of the type safety from TanStack Router.
wise-white
wise-white13mo ago
Thanks that works. I did not have to change anything in the other components.

Did you find this page helpful?