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.
---
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:
- All of the JS code is moved into the header between the opening and closing
---
- 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.
An easy way to edit these values in global.css
is to wrap your values in hsl()
. Then you can use the built in color picker to modify your 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.
---
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' aesthetic.
</AccordionContent>
</AccordionItem>
<AccordionItem value="item-3">
<AccordionTrigger>Is it animated?</AccordionTrigger>
<AccordionContent>
Yes. It's animated by default, but you can disable it if you
prefer.
</AccordionContent>
</AccordionItem>
</Accordion>
</main>
</Layout>
Error
An error occurred.
Accordion
must be used within Accordion
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.
- Rewrite the ShadCN components so they work with Astro. This might involve re-architecting an existing solution that's built into a library.
- 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
.
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:
---
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.
Don't forget the client:idle
attribute on our Accordion implementation. This tells Astro to load the necessary JS code on the client side so users can click to open/close each accordion item.
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.