Add a Shadcn UI Component

provides an excellent set of components that are an opinionated implementation of React components. Space Madness includes a few of these components as an efficient way to include high quality, accessible components with minimal development overhead.

You can add more components to your site via the . Your site comes preconfigured with the config file at site-astro/src/components.json, so adding a new component is relatively easy. There are a few gotchas when working with Astro. We'll cover those in a later section.

Add a basic component

Let's add a new component to our site and render it. We'll use the Table component as an example.

Open your terminal of choice and navigate to the site-astro directory.

Add a new component.

npx shadcn-ui@latest add table

A new table.tsx file has been created for us. Let's see it in action. Open your homepage index.astro and add in the following code.

site-astro/src/pages/index.astro
---
import Layout from "../layouts/Layout.astro";

import {
	Table,
	TableBody,
	TableCaption,
	TableCell,
	TableHead,
	TableHeader,
	TableRow,
} from "@/components/ui/table";

const invoices = [
	{
		invoice: "INV001",
		paymentStatus: "Paid",
		totalAmount: "$250.00",
		paymentMethod: "Credit Card",
	},
	{
		invoice: "INV002",
		paymentStatus: "Pending",
		totalAmount: "$150.00",
		paymentMethod: "PayPal",
	},
	{
		invoice: "INV003",
		paymentStatus: "Unpaid",
		totalAmount: "$350.00",
		paymentMethod: "Bank Transfer",
	},
	{
		invoice: "INV004",
		paymentStatus: "Paid",
		totalAmount: "$450.00",
		paymentMethod: "Credit Card",
	},
	{
		invoice: "INV005",
		paymentStatus: "Paid",
		totalAmount: "$550.00",
		paymentMethod: "PayPal",
	},
	{
		invoice: "INV006",
		paymentStatus: "Pending",
		totalAmount: "$200.00",
		paymentMethod: "Bank Transfer",
	},
	{
		invoice: "INV007",
		paymentStatus: "Unpaid",
		totalAmount: "$300.00",
		paymentMethod: "Credit Card",
	},
];
---

<Layout>
	<main>
		<h1>Welcome to SiteName</h1>

		<Table>
			<TableCaption>A list of your recent invoices.</TableCaption>
			<TableHeader>
				<TableRow>
					<TableHead className="w-[100px]">Invoice</TableHead>
					<TableHead>Status</TableHead>
					<TableHead>Method</TableHead>
					<TableHead className="text-right">Amount</TableHead>
				</TableRow>
			</TableHeader>
			<TableBody>
				{
					invoices.map((invoice) => (
						<TableRow key={invoice.invoice}>
							<TableCell className="font-medium">{invoice.invoice}</TableCell>
							<TableCell>{invoice.paymentStatus}</TableCell>
							<TableCell>{invoice.paymentMethod}</TableCell>
							<TableCell className="text-right">
								{invoice.totalAmount}
							</TableCell>
						</TableRow>
					))
				}
			</TableBody>
		</Table>
	</main>
</Layout>

This code is taken directly from the example on the Shadcn site. For our Astro site, the only differences are:

  1. All of the JS code is moved into the header between the opening and closing ---
  2. The JSX code is not wrapped in an exported function, we can delete those lines and use what is in the return statement.

Load up your homepage and test it out. Looks good!

Update the styles

The component code is configured in a few places. components.json specifies a color scheme. tailwind.config.mjs extends tailwind theme colors, borders, fonts, and animations. global.css is where most of our style variables are configured.

All style variables use HSL color input values.

Where you make your changes depends on how much control you want over the resulting code.

In general, if you want to keep to the Tailwind styles as closely as possible, make edits to the CSS variables in global.css.

If you want to get fancy with the spices, edit tailwind.config.mjs or write your own CSS using the Tailwind @extends directive.

One tool you might find helpful is this for shadcn/ui.

Working with Interactive Components

The table component does not require any client side JS for interactivity. This makes it an ideal starting place to work with components. Now let's see what happens when we add an interactive component. Let's add an Accordion.

npx shadcn-ui@latest add accordion

Following the pattern from before, let's import the example to our index page.

site-astro/src/pages/index.astro
---
import Layout from "../layouts/Layout.astro";
import {
	Accordion,
	AccordionContent,
	AccordionItem,
	AccordionTrigger,
} from "@/components/ui/accordion";
---

<Layout>
	<main>
		<h1>Welcome to SiteName</h1>
		<Accordion type="single" collapsible className="w-full">
			<AccordionItem value="item-1">
				<AccordionTrigger>Is it accessible?</AccordionTrigger>
				<AccordionContent>
					Yes. It adheres to the WAI-ARIA design pattern.
				</AccordionContent>
			</AccordionItem>
			<AccordionItem value="item-2">
				<AccordionTrigger>Is it styled?</AccordionTrigger>
				<AccordionContent>
					Yes. It comes with default styles that matches the other
					components&apos; aesthetic.
				</AccordionContent>
			</AccordionItem>
			<AccordionItem value="item-3">
				<AccordionTrigger>Is it animated?</AccordionTrigger>
				<AccordionContent>
					Yes. It&apos;s animated by default, but you can disable it if you
					prefer.
				</AccordionContent>
			</AccordionItem>
		</Accordion>
	</main>
</Layout>

Oh no! We have an error. Let's talk through the issue.

Astro uses an "", which means that each block of code on the site is an independent group of code. Astro does not assume that our Accordion component pieces fit together. Our Shadcn UI components assume that the components can talk to each other and share context. They cannot.

We can solve this in two ways.

  1. Rewrite the ShadCN components so they work with Astro. This might involve re-architecting an existing solution that's built into a library.
  2. Wrap all of our Accordion components into one component where we can pass data to it. We lose the composability, but we avoid having to rewrite a lot of logic.

For this guide, we'll go with option 2. If you want to see an example of option 1, check out Popover.tsx.

Let's create a new Accordion.tsx component.

We'll update the example to iterate over a data prop and render each AccordionItem.

site-astro/src/components/Accordion.tsx
import {
  Accordion as BaseAccordion,
  AccordionContent,
  AccordionItem,
  AccordionTrigger,
} from "@/components/ui/accordion";

interface Props {
  data: {
    title: string;
    description: string;
  }[];
}

export function Accordion({ data }: Props) {
  return (
    <BaseAccordion type="single" collapsible className="w-full">
      {data.map((item, i) => {
        return (
          <AccordionItem value={`item-${i.toFixed()}`}>
            <AccordionTrigger>{item.title}</AccordionTrigger>
            <AccordionContent>{item.description}</AccordionContent>
          </AccordionItem>
        );
      })}
    </BaseAccordion>
  );
}

Use it index.astro like this:

site-astro/src/pages/index.astro
---
import Layout from "../layouts/Layout.astro";
import { Accordion } from "@/components/content/Accordion";

const accordionData = [
	{
		title: "Is it accessible?",
		description: "Yes. It adheres to the WAI-ARIA design pattern.",
	},
	{
		title: "Is it styled?",
		description:
			"Yes. It comes with default styles that matches the other components' aesthetic.",
	},
	{
		title: "Is it animated?",
		description:
			"Yes. It's animated by default, but you can disable it if you prefer.",
	},
];
---

<Layout>
	<main>
		<h1>Welcome to SiteName</h1>
		<Accordion data={accordionData} client:idle />
	</main>
</Layout>

Now we're in business.

Astro now treats our entire accordion as one "island". The React context needed to wrap all accordion items can now execute in our browser without an issue.

Wrapping Up

Hopefully, this gets you on your way to building quickly with Shadcn and Radix. Both tools should help you write better, more accessible components. They can be daunting to get started with, but they're well documented tools with thriving communities that can help you with your specific use case.