Handbook
TypeScript
The types that ferry-ui exports, and how to use them in your own components.
Handbook
The types that ferry-ui exports, and how to use them in your own components.
The package contains its type declarations. Import each type from the package root, with the components.
import { Button, type ButtonProps, type NavGroup, type StatusTone } from 'ferry-ui'A component that adds props of its own exports a prop type: ButtonProps, CalloutProps, FieldProps. Use this type to make a component that extends a ferry-ui component.
import { Button, type ButtonProps } from 'ferry-ui'
import { Download } from 'lucide-react'
// Each prop of Button, but this component sets the icon and the text.
type ExportButtonProps = Omit<ButtonProps, 'icon' | 'children'> & {
format: 'CSV' | 'PDF'
}
function ExportButton({ format, ...props }: ExportButtonProps) {
return (
<Button icon={<Download />} {...props}>
Export {format}
</Button>
)
}
export default function PropTypes() {
return (
<>
<ExportButton format="CSV" />
<ExportButton format="PDF" variant="outline" />
</>
)
}Some components only pass the props of their Radix part or of their HTML element. They export no prop type. Checkbox, Label and the parts of Card are examples.
Read the props of these components with ComponentProps from React.
import type { ComponentProps } from 'react'
import { Checkbox, Label } from 'ferry-ui'
// Checkbox exports no prop type: read the props from the component.
type OptionProps = ComponentProps<typeof Checkbox> & {
label: string
}
function Option({ label, ...props }: OptionProps) {
return (
<Label>
<Checkbox {...props} />
{label}
</Label>
)
}
export default function ComponentPropsType() {
return (
<div className="flex flex-col gap-3">
<Option label="Send me the weekly digest" defaultChecked />
<Option label="Send me each invoice" />
</div>
)
}Some props accept a closed list of values. ferry-ui exports a type for many of these lists.
| Type | Values |
|---|---|
StatusTone | success, warning, destructive, info, neutral |
CalloutTone | info, warning, destructive, success, neutral |
ConfirmDialogTone | destructive, warning, primary |
ThemePreference | light, dark, system |
ResolvedTheme | light, dark |
Use these types to connect the values of your domain to ferry-ui in one place. This example gives one tone to each status of an invoice.
import { StatusBadge, type StatusTone } from 'ferry-ui'
type InvoiceStatus = 'paid' | 'open' | 'overdue'
// TypeScript refuses a tone that ferry-ui does not have, and a status with no entry.
const INVOICE_STATUS: Record<InvoiceStatus, { tone: StatusTone; label: string }> = {
paid: { tone: 'success', label: 'Paid' },
open: { tone: 'info', label: 'Open' },
overdue: { tone: 'destructive', label: 'Overdue' },
}
const STATUSES: InvoiceStatus[] = ['paid', 'open', 'overdue']
export default function StatusMap() {
return (
<>
{STATUSES.map((status) => (
<StatusBadge key={status} {...INVOICE_STATUS[status]} />
))}
</>
)
}RadioCardOption and Radio Card Group take the type of the value as a type parameter. The onValueChange callback then gets this type, not a string.
import * as React from 'react'
import { RadioCardGroup, type RadioCardOption } from 'ferry-ui'
import { Globe, Lock } from 'lucide-react'
type Visibility = 'private' | 'public'
const OPTIONS: RadioCardOption<Visibility>[] = [
{ value: 'private', label: 'Private', description: 'Only invited members can open it.', icon: <Lock /> },
{ value: 'public', label: 'Public', description: 'Anyone with the link can view it.', icon: <Globe /> },
]
export default function GenericValue() {
// `onValueChange` gives a `Visibility`, not a `string`.
const [visibility, setVisibility] = React.useState<Visibility>('private')
return (
<RadioCardGroup
aria-label="Visibility"
className="w-full max-w-lg"
columns={2}
options={OPTIONS}
value={visibility}
onValueChange={setVisibility}
/>
)
}A variant helper returns the classes of a component, for an element that is not this component. Each option has the type of the prop with the same name.
| Helper | Options |
|---|---|
buttonVariants | variant, size, shape |
badgeVariants | variant, font, shape, case |
inputVariants | size, mono |
tabsListVariants | variant |
toggleVariants | variant, size |
statusBadgeVariants | tone, size |
iconBoxVariants | size, tone, elevated |
import { buttonVariants, type ButtonProps } from 'ferry-ui'
// The options of the helper have the same types as the props of Button.
type RepositoryLinkProps = Pick<ButtonProps, 'variant' | 'size'>
function RepositoryLink({ variant, size }: RepositoryLinkProps) {
return (
<a
className={buttonVariants({ variant, size })}
href="https://github.com/Carter2307/ferry-ui"
target="_blank"
rel="noreferrer"
>
Open the repository
</a>
)
}
export default function VariantHelper() {
return (
<>
<RepositoryLink />
<RepositoryLink variant="outline" size="md" />
</>
)
}To get the type of one option, read it from the prop type: ButtonProps['variant']. For iconBoxVariants, ferry-ui exports the IconBoxVariantProps type.
ferry-ui makes these helpers with cva from class-variance-authority. The VariantProps type of that package gives all the options of one helper. Add the package to the dependencies of your app before you import it.
import type { VariantProps } from 'class-variance-authority'
import { badgeVariants } from 'ferry-ui'
type BadgeOptions = VariantProps<typeof badgeVariants>Call a variant helper only from a client component. The Server Components page gives the reason.
NavItem and NavGroup describe the navigation of your app. Icon Rail, Mobile Nav, Inner Menu and Command Menu accept the same value.
import type { NavGroup } from 'ferry-ui'
import { FolderKanban, Settings } from 'lucide-react'
export const groups: NavGroup[] = [
{
id: 'main',
items: [
{ id: 'projects', label: 'Projects', icon: <FolderKanban />, href: '/projects', active: true },
{ id: 'settings', label: 'Settings', icon: <Settings />, href: '/settings' },
],
},
]LinkComponent is the type of a router adapter. The adapter gets href as a string, and each prop of an anchor.
import { Link } from 'react-router'
import type { LinkComponent } from 'ferry-ui'
export const RouterLink: LinkComponent = ({ href, ...props }) => <Link to={href} {...props} />A router with typed routes can refuse a plain string. If it does, cast href in the adapter. The Routing page shows the adapters for other routers.