Card
A bordered surface holding one piece of the page.
import { Div } from "@implementjs/core";
import { Button } from "@/lib/components/ui/button";
import {
Card,
CardAction,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/lib/components/ui/card";
import { Input } from "@/lib/components/ui/input";
import { Label } from "@/lib/components/ui/label";
export default function CardDemo() {
return Card(
{ class: "w-full max-w-sm" },
CardHeader(
CardTitle("Sign in"),
CardDescription("Use the email you signed up with."),
CardAction(Button({ variant: "link", size: "sm" }, "Sign up")),
),
CardContent(
Div(
{ class: "flex flex-col gap-4" },
Div(
{ class: "flex flex-col gap-2" },
Label({ for: "card-email" }, "Email"),
Input({ id: "card-email", type: "email", placeholder: "you@example.com" }),
),
Div(
{ class: "flex flex-col gap-2" },
Label({ for: "card-password" }, "Password"),
Input({ id: "card-password", type: "password" }),
),
),
),
CardFooter({ class: "gap-2" }, Button({ class: "flex-1" }, "Sign in")),
);
}Installation
npx jsrepo add @implementjs/ui/card
Nothing else comes with it — this one stands alone on @implementjs/core and @implementjs/primitives.
Copy the file below to src/lib/components/ui/card.ts. It imports cn, so copy utils.ts to src/lib/utils.ts too.
import { Div, type Child, type ElementProps } from "@implementjs/core";
import { createComponent } from "@implementjs/primitives";
import { cn } from "@/lib/utils";
export type CardProps = ElementProps<"div">;
export type CardHeaderProps = ElementProps<"div">;
export type CardTitleProps = ElementProps<"div">;
export type CardDescriptionProps = ElementProps<"div">;
export type CardActionProps = ElementProps<"div">;
export type CardContentProps = ElementProps<"div">;
export type CardFooterProps = ElementProps<"div">;
/** A bordered surface holding one self-contained piece of the page. */
export const Card = createComponent(function Card(
{ class: className, ...props }: CardProps,
...children: Child[]
) {
return Div(
{
...props,
"data-slot": "card",
class: cn(
"flex flex-col gap-6 rounded-xl border bg-card py-6 text-card-foreground shadow-sm",
className,
),
},
...children,
);
});
/**
* The header row. It is a grid rather than a flex row so that `CardAction`
* can sit in a second column without the title and description having to
* know it is there — the grid gains the column only when an action exists.
*/
export const CardHeader = createComponent(function CardHeader(
{ class: className, ...props }: CardHeaderProps,
...children: Child[]
) {
return Div(
{
...props,
"data-slot": "card-header",
class: cn(
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6",
"has-data-[slot=card-action]:grid-cols-[1fr_auto]",
"[.border-b]:pb-6",
className,
),
},
...children,
);
});
export const CardTitle = createComponent(function CardTitle(
{ class: className, ...props }: CardTitleProps,
...children: Child[]
) {
return Div(
{ ...props, "data-slot": "card-title", class: cn("leading-none font-semibold", className) },
...children,
);
});
export const CardDescription = createComponent(function CardDescription(
{ class: className, ...props }: CardDescriptionProps,
...children: Child[]
) {
return Div(
{
...props,
"data-slot": "card-description",
class: cn("text-sm text-muted-foreground", className),
},
...children,
);
});
/** A control in the top right of the header — a menu, a link, a switch. */
export const CardAction = createComponent(function CardAction(
{ class: className, ...props }: CardActionProps,
...children: Child[]
) {
return Div(
{
...props,
"data-slot": "card-action",
class: cn("col-start-2 row-span-2 row-start-1 self-start justify-self-end", className),
},
...children,
);
});
export const CardContent = createComponent(function CardContent(
{ class: className, ...props }: CardContentProps,
...children: Child[]
) {
return Div({ ...props, "data-slot": "card-content", class: cn("px-6", className) }, ...children);
});
export const CardFooter = createComponent(function CardFooter(
{ class: className, ...props }: CardFooterProps,
...children: Child[]
) {
return Div(
{
...props,
"data-slot": "card-footer",
class: cn("flex items-center px-6 [.border-t]:pt-6", className),
},
...children,
);
});Usage
import {
Card,
CardAction,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/lib/components/ui/card";
Card(
CardHeader(CardTitle("Sign in"), CardDescription("Use the email you signed up with.")),
CardContent(Form()),
CardFooter(Button({ class: "flex-1" }, "Sign in")),
);
The header is a grid
CardHeader is a grid rather than a flex row, and it grows a second column only when a CardAction is present (has-data-[slot=card-action]). That way a control in the top right does not force the title and description to know about it:
CardHeader(
CardTitle("Sign in"),
CardDescription("Use the email you signed up with."),
CardAction(Button({ variant: "link", size: "sm" }, "Sign up")),
);
Borders on the header and footer
Add border-b to the header or border-t to the footer and the padding adjusts itself — the components watch for those classes ([.border-b]:pb-6) rather than making you add the padding too.
API Reference
Card
A bordered surface holding one self-contained piece of the page. Renders a Div; extra props are forwarded onto it.
CardHeader
A grid rather than a flex row, so CardAction can take a second column without the title and description having to know it is there. Renders a Div; extra props are forwarded onto it.
CardTitle
The card's name. Renders a Div; extra props are forwarded onto it.
CardDescription
A line under the title, in muted text. Renders a Div; extra props are forwarded onto it.
CardAction
A control in the top right of the header — a menu, a link, a switch. Renders a Div; extra props are forwarded onto it.
CardContent
The body. Renders a Div; extra props are forwarded onto it.
CardFooter
The bottom row, usually the actions. Renders a Div; extra props are forwarded onto it.