Shadcn Type Table for React and Tailwind
Expandable Prop / Type rows for component APIs.
Default
trueInstallation
bunx --bun shadcn@latest add https://kit.dev/r/type-table.jsonpnpm dlx shadcn@latest add https://kit.dev/r/type-table.jsonnpx shadcn@latest add https://kit.dev/r/type-table.jsonyarn shadcn@latest add https://kit.dev/r/type-table.jsonThis component depends on Accordion. Install it first if you haven't already.
Install the following dependencies:
bun add @ark-ui/reactpnpm add @ark-ui/reactnpm install @ark-ui/reactyarn add @ark-ui/reactCopy and paste the following code into your project.
"use client";
import type React from "react";
import { cn } from "@/lib/utils";
import {
Accordion,
AccordionContent,
AccordionContext,
AccordionItem,
AccordionItemContext,
AccordionTrigger,
useAccordionContext,
useAccordionItemContext,
} from "@/components/ui/accordion";
export const TypeTableContext = AccordionContext;
export const TypeTableItemContext = AccordionItemContext;
export const useTypeTableContext = useAccordionContext;
export const useTypeTableItemContext = useAccordionItemContext;
export interface TypeTableParameter {
description?: React.ReactNode;
name: string;
}
export interface TypeTableProps
extends Omit<
React.ComponentProps<typeof Accordion>,
"multiple" | "orientation"
> {
/**
* Allow more than one row to be expanded.
*
* @default true
*/
multiple?: boolean;
/**
* Row density. Compact reduces trigger padding.
*
* @default "default"
*/
size?: "default" | "compact";
}
export interface TypeTableItemProps
extends Omit<
React.ComponentProps<typeof AccordionItem>,
"value" | "children"
> {
/** Description shown when the row is open. */
children?: React.ReactNode;
/** Shown as Default in the open panel. */
defaultValue?: string;
/**
* Strike the name.
*
* @default false
*/
deprecated?: boolean;
/** Name in the first column. Also the item value unless `value` is set. */
name: string;
/**
* Renders `?` after the name.
*
* @default false
*/
optional?: boolean;
/** Parameter list when the type is a function. */
parameters?: TypeTableParameter[];
/** Return type shown in the open panel. */
returns?: React.ReactNode;
/** Type in the second column. */
type: string;
/** Full type signature in the open panel. Overrides the truncated column type. */
typeDescription?: React.ReactNode;
/** Optional href for the type in the second column. */
typeDescriptionLink?: string;
/** Unique accordion value. Defaults to `name`. */
value?: string;
}
const TYPE_DETAIL_RE = /[|&<>]/;
const typeTableRowClassName = "flex min-w-0 flex-1 items-center";
const typeNeedsDetail = (type: string) =>
type.length > 32 || TYPE_DETAIL_RE.test(type) || type.includes("=>");
const isPresent = (value: React.ReactNode) =>
value !== undefined && value !== null && value !== "";
const resolveTypeDetail = (
typeDescription: React.ReactNode | undefined,
type: string
) => {
if (isPresent(typeDescription)) {
return typeDescription;
}
if (typeNeedsDetail(type)) {
return type;
}
};
const stopRowToggle = (event: React.MouseEvent) => {
event.stopPropagation();
};
const CodeValue = (props: { children: string; block?: boolean }) => {
const { children, block = false } = props;
return (
<code
className={cn(
"min-w-0 font-mono text-foreground",
"rounded-md border border-border bg-muted",
block
? "block w-full whitespace-normal break-words px-2 py-1.5"
: "inline-block max-w-full whitespace-normal break-words px-1.5 py-0.5"
)}
>
{children}
</code>
);
};
const TypeMeta = (props: { label: string; children: React.ReactNode }) => {
const { label, children } = props;
return (
<>
<p className="text-muted-foreground">{label}</p>
<div className="min-w-0">
{typeof children === "string" ? (
<CodeValue block={children.length > 40}>{children}</CodeValue>
) : (
children
)}
</div>
</>
);
};
const TypeTableHeader = () => (
<div
className={cn(
"flex w-full min-w-0 items-center justify-between gap-3",
"px-3 py-1",
"font-medium text-muted-foreground text-xs",
"group-data-[size=compact]/type-table:py-0.5"
)}
data-slot="type-table-header"
>
<span className={typeTableRowClassName}>
<span className="w-1/4 min-w-fit pe-2">Prop</span>
<span className="@max-[20rem]:hidden min-w-0">Type</span>
</span>
<span aria-hidden="true" className="size-4 shrink-0" />
</div>
);
const typeColumnClassName =
"@max-[20rem]:hidden min-w-0 truncate font-mono text-muted-foreground";
const TypeColumn = (props: { href?: string; type: string }) => {
const { href, type } = props;
if (href) {
return (
<a
className={cn(
typeColumnClassName,
"underline underline-offset-4 hover:text-foreground"
)}
data-slot="type-table-type"
href={href}
onClick={stopRowToggle}
title={type}
>
{type}
</a>
);
}
return (
<span
className={typeColumnClassName}
data-slot="type-table-type"
title={type}
>
{type}
</span>
);
};
const ParameterList = (props: { parameters: TypeTableParameter[] }) => {
const { parameters } = props;
return (
<div className="flex flex-col gap-2">
{parameters.map((parameter) => (
<div className="flex flex-wrap items-center gap-1" key={parameter.name}>
<span className="text-nowrap font-medium font-mono">
{parameter.name}
</span>
{isPresent(parameter.description) ? (
<>
<span aria-hidden="true">–</span>
<span>{parameter.description}</span>
</>
) : null}
</div>
))}
</div>
);
};
const TypeTableItemPanel = (props: {
body?: React.ReactNode;
defaultValue?: string;
parameters?: TypeTableParameter[];
returns?: React.ReactNode;
typeDetail?: React.ReactNode;
}) => {
const { body, defaultValue, parameters, returns, typeDetail } = props;
const hasParameters = (parameters?.length ?? 0) > 0;
const hasMeta =
typeDetail !== undefined ||
defaultValue !== undefined ||
hasParameters ||
isPresent(returns);
if (body === undefined && !hasMeta) {
return null;
}
return (
<AccordionContent className="rounded-none border-t [&>div]:p-3">
<div className="grid grid-cols-[1fr_3fr] items-start gap-x-3 gap-y-4 text-sm">
{body === undefined ? null : (
<div className="col-span-full text-pretty">{body}</div>
)}
{typeDetail === undefined ? null : (
<TypeMeta label="Type">{typeDetail}</TypeMeta>
)}
{defaultValue === undefined ? null : (
<TypeMeta label="Default">{defaultValue}</TypeMeta>
)}
{hasParameters ? (
<TypeMeta label="Parameters">
<ParameterList parameters={parameters ?? []} />
</TypeMeta>
) : null}
{isPresent(returns) ? (
<TypeMeta label="Returns">{returns}</TypeMeta>
) : null}
</div>
</AccordionContent>
);
};
export const TypeTable = (props: TypeTableProps) => {
const {
multiple = true,
size = "default",
collapsible = true,
className,
children,
...rest
} = props;
return (
<Accordion
className={cn(
"group/type-table not-prose",
"@container flex w-full flex-col overflow-hidden rounded-xl border bg-card p-1 text-card-foreground text-sm",
className
)}
collapsible={collapsible}
data-size={size}
data-slot="type-table"
multiple={multiple}
{...rest}
>
<TypeTableHeader />
{children}
</Accordion>
);
};
export const TypeTableItem = (props: TypeTableItemProps) => {
const {
name,
type,
optional = false,
defaultValue,
deprecated = false,
value,
children,
className,
parameters,
returns,
typeDescription,
typeDescriptionLink,
...rest
} = props;
return (
<AccordionItem
className={cn(
"scroll-mt-20 overflow-hidden rounded-lg border border-transparent last:border-b",
"transition-all",
"data-[state=open]:border-border data-[state=open]:bg-background data-[state=open]:shadow-sm",
"data-[state=open]:not-last:mb-2",
className
)}
data-slot="type-table-item"
value={value ?? name}
{...rest}
>
<AccordionTrigger
className={cn(
"rounded-lg px-3 font-normal",
"hover:bg-accent",
"data-[orientation=vertical]:min-h-9 data-[orientation=vertical]:py-2",
"group-data-[size=compact]/type-table:data-[orientation=vertical]:min-h-7",
"group-data-[size=compact]/type-table:data-[orientation=vertical]:py-1"
)}
>
<span className={typeTableRowClassName}>
<code
className={cn(
"w-1/4 min-w-fit break-words pe-2 text-start font-medium font-mono text-primary",
deprecated && "text-primary/50 line-through"
)}
data-slot="type-table-name"
>
{name}
{optional ? "?" : null}
</code>
<TypeColumn href={typeDescriptionLink} type={type} />
</span>
</AccordionTrigger>
<TypeTableItemPanel
body={isPresent(children) ? children : undefined}
defaultValue={defaultValue}
parameters={parameters}
returns={returns}
typeDetail={resolveTypeDetail(typeDescription, type)}
/>
</AccordionItem>
);
};Update the import paths to match your project setup.
Anatomy
TypeTable
└── TypeTableItemshadcn.io Type Table is a styled Accordion. TypeTable is the root; each TypeTableItem is one expandable row. Column labels (Prop / Type) are rendered by the root. The chevron comes from AccordionTrigger.
Open rows inset as a nested card (bg-background, border, shadow) inside the padded bg-card frame. Names use text-primary so they follow the shadcn theme — Neutral, every accent, and dark.
Usage
import {
TypeTable,
TypeTableItem,
} from "@/components/ui/type-table";<TypeTable>
<TypeTableItem name="value" type="string">
Which item is expanded.
</TypeTableItem>
<TypeTableItem name="disabled" type="boolean" optional defaultValue="false">
Disable this row.
</TypeTableItem>
</TypeTable>value and defaultValue on the root are expanded row ids (string[]), same as Accordion. defaultValue on an item is the documented default shown in the open panel.
Examples
Optional
Mark a prop optional with optional. A ? is rendered after the name.
Single
Set multiple={false} so only one row can be open at a time. Type Table defaults multiple to true.
Compact
size="compact" reduces row padding. Names and types stay the same size.
Disabled
disabled on an item greys the row out. disabled on the root disables every row.
Deprecated
deprecated strikes the name.
Controlled
value / onValueChange use Ark’s { value: string[] } details, not a bare string.
Guides
Accordion
Type Table is Accordion with a Prop / Type header and framed card styles. Remaining Accordion root props (collapsible, dir, lazyMount, …) pass through. orientation is omitted — rows are always vertical.
Open state is always a string[]. Item value defaults to name.
<TypeTable defaultValue={["value"]}>
<TypeTableItem name="value" type="string">
Starts open.
</TypeTableItem>
</TypeTable>Type column
The Type column truncates. Long types, unions (|), generics (<>), and function types (=>) also render a Type row in the open panel. Pass typeDescription to show a full signature instead of the truncated column type. typeDescriptionLink turns the column type into a link.
defaultValue on the item always shows as Default. Function types can add parameters and returns.
At narrow widths (@max-[20rem]) the Type column is hidden; expand the row to read it.
Names use text-primary (deprecated: text-primary/50). Types stay text-muted-foreground, so Neutral still separates the columns and Teal / Blue / etc. paint the first column.
Type Table vs Data List vs Keyboard Table vs Table
| Type Table | Data List | Keyboard Table | Table | |
|---|---|---|---|---|
| Shape | Expandable Prop / Type rows | Label / value pairs | Two-column shortcut table | Rows and columns |
| Use when | Component API docs | Metadata, summaries | Keyboard support | Tabular comparison |
API Reference
shadcn.io Type Table wraps Ark UI Accordion. Defaults below are shadcn.io values. multiple is true here (false on Accordion). value / defaultValue are string[].
asChild merges props onto a single child element.
TypeTable
Root. Renders Accordion’s root div. Native Accordion root props pass through except orientation.
Default
true| Attribute | Description |
|---|---|
data-slot | type-table |
data-size | "default" or "compact" |
data-scope | accordion |
data-part | root |
data-orientation | "vertical" |
Root styles: @container, rounded-xl border bg-card p-1 text-card-foreground. The Prop / Type header is data-slot="type-table-header". Open items use bg-background, a visible border, and shadow-sm.
TypeTableItem
One API row. Renders Accordion’s item div.
? after the name.Default
false| Attribute | Description |
|---|---|
data-slot | type-table-item |
data-state | "open" or "closed" |
data-disabled | Present when disabled |
Name uses data-slot="type-table-name". Type uses data-slot="type-table-type".
TypeTableContext / useTypeTableContext
Same as Accordion context. TypeTableContext children: (context) => ReactNode.
TypeTableItemContext / useTypeTableItemContext
Same as Accordion item context. TypeTableItemContext children: (context) => ReactNode.
Accessibility
Complies with the Accordion WAI-ARIA design pattern. Each row is a trigger; the open panel is the associated content. Keep descriptions as text or structured content inside the item — do not put a second interactive control in the trigger.
Keyboard support
| Key | Description |
|---|---|
Space | When focus is on a collapsed trigger, expand the row. |
Enter | When focus is on a collapsed trigger, expand the row. |
Tab | Move focus to the next focusable element. |
Shift+Tab | Move focus to the previous focusable element. |
ArrowDown | Move focus to the next trigger. |
ArrowUp | Move focus to the previous trigger. |
Home | Move focus to the first trigger. |
End | Move focus to the last trigger. |