Kosmesis
Components

Card

A bordered content container with Header, Title, Description, Action, Content, and Footer parts. Purely presentational.

Installation

npx kosmesis add card
pnpm dlx kosmesis add card
yarn dlx kosmesis add card
bunx kosmesis add card

Copy and paste the following code into your project.

card.tsx
import { StatelessComponent } from "@praxisjs/core";import { Component } from "@praxisjs/decorators";import type { Children } from "@praxisjs/shared";import { cn } from "@/lib/utils";export interface CardSlotProps {  class?: string;  id?: string;  children?: Children;}/** Purely presentational — no Morphos equivalent, same as upstream shadcn/ui. */@Component()export class Card extends StatelessComponent<CardSlotProps> {  render() {    const { class: cls, id, children } = this.props;    return (      <div        id={id}        data-slot="card"        class={cn("flex flex-col gap-6 rounded-xl border bg-card py-6 text-card-foreground shadow-sm", cls)}      >        {children}      </div>    );  }}@Component()export class CardHeader extends StatelessComponent<CardSlotProps> {  render() {    const { class: cls, id, children } = this.props;    return (      <div        id={id}        data-slot="card-header"        class={cn(          "grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-[[data-slot=card-action]]:grid-cols-[1fr_auto]",          cls,        )}      >        {children}      </div>    );  }}@Component()export class CardTitle extends StatelessComponent<CardSlotProps> {  render() {    const { class: cls, id, children } = this.props;    return (      <div id={id} data-slot="card-title" class={cn("font-semibold leading-none", cls)}>        {children}      </div>    );  }}@Component()export class CardDescription extends StatelessComponent<CardSlotProps> {  render() {    const { class: cls, id, children } = this.props;    return (      <div id={id} data-slot="card-description" class={cn("text-sm text-muted-foreground", cls)}>        {children}      </div>    );  }}@Component()export class CardAction extends StatelessComponent<CardSlotProps> {  render() {    const { class: cls, id, children } = this.props;    return (      <div        id={id}        data-slot="card-action"        class={cn("col-start-2 row-span-2 row-start-1 self-start justify-self-end", cls)}      >        {children}      </div>    );  }}@Component()export class CardContent extends StatelessComponent<CardSlotProps> {  render() {    const { class: cls, id, children } = this.props;    return (      <div id={id} data-slot="card-content" class={cn("px-6", cls)}>        {children}      </div>    );  }}@Component()export class CardFooter extends StatelessComponent<CardSlotProps> {  render() {    const { class: cls, id, children } = this.props;    return (      <div id={id} data-slot="card-footer" class={cn("flex items-center px-6 [.border-t]:pt-6", cls)}>        {children}      </div>    );  }}

Install the following dependencies:

npm install @praxisjs/css
pnpm add @praxisjs/css
yarn add @praxisjs/css
bun add @praxisjs/css

Copy and paste the following code into your project.

card.tsx
import { StatelessComponent } from "@praxisjs/core";import { cx, Stylesheet, Styled, tokenVars } from "@praxisjs/css";import { Component } from "@praxisjs/decorators";import type { Children } from "@praxisjs/shared";import { KosmesisTokens } from "@/lib/kosmesis-theme";const t = tokenVars(KosmesisTokens);class CardStyles extends Stylesheet {  $root = this.css({    display: "flex",    flexDirection: "column",    gap: "1.5rem",    borderRadius: "0.75rem",    border: `1px solid ${t.border}`,    backgroundColor: t.card,    paddingTop: "1.5rem",    paddingBottom: "1.5rem",    color: t.cardForeground,    boxShadow: "0 1px 2px 0 rgb(0 0 0 / 0.05)",  });  $header = this.css({    display: "grid",    gridAutoRows: "min-content",    gridTemplateRows: "auto auto",    alignItems: "start",    gap: "0.375rem",    paddingLeft: "1.5rem",    paddingRight: "1.5rem",  }).has("[data-slot=card-action]", { gridTemplateColumns: "1fr auto" });  $title = this.css({ fontWeight: 600, lineHeight: 1 });  $description = this.css({ fontSize: "0.875rem", color: t.mutedForeground });  $action = this.css({    gridColumnStart: 2,    gridRowStart: 1,    gridRow: "span 2 / span 2",    alignSelf: "start",    justifySelf: "end",  });  $content = this.css({ paddingLeft: "1.5rem", paddingRight: "1.5rem" });  $footer = this.css({ display: "flex", alignItems: "center", paddingLeft: "1.5rem", paddingRight: "1.5rem" }).on(    "&.kosmesis-bordered",    { paddingTop: "1.5rem" },  );}export interface CardSlotProps {  class?: string;  id?: string;  children?: Children;}/** Purely presentational — no Morphos equivalent, same as upstream shadcn/ui. */@Component()export class Card extends StatelessComponent<CardSlotProps> {  @Styled(CardStyles) $s!: CardStyles;  render() {    const { class: cls, id, children } = this.props;    return (      <div id={id} data-slot="card" class={cx(this.$s.$root, cls)}>        {children}      </div>    );  }}@Component()export class CardHeader extends StatelessComponent<CardSlotProps> {  @Styled(CardStyles) $s!: CardStyles;  render() {    const { class: cls, id, children } = this.props;    return (      <div id={id} data-slot="card-header" class={cx(this.$s.$header, cls)}>        {children}      </div>    );  }}@Component()export class CardTitle extends StatelessComponent<CardSlotProps> {  @Styled(CardStyles) $s!: CardStyles;  render() {    const { class: cls, id, children } = this.props;    return (      <div id={id} data-slot="card-title" class={cx(this.$s.$title, cls)}>        {children}      </div>    );  }}@Component()export class CardDescription extends StatelessComponent<CardSlotProps> {  @Styled(CardStyles) $s!: CardStyles;  render() {    const { class: cls, id, children } = this.props;    return (      <div id={id} data-slot="card-description" class={cx(this.$s.$description, cls)}>        {children}      </div>    );  }}@Component()export class CardAction extends StatelessComponent<CardSlotProps> {  @Styled(CardStyles) $s!: CardStyles;  render() {    const { class: cls, id, children } = this.props;    return (      <div id={id} data-slot="card-action" class={cx(this.$s.$action, cls)}>        {children}      </div>    );  }}@Component()export class CardContent extends StatelessComponent<CardSlotProps> {  @Styled(CardStyles) $s!: CardStyles;  render() {    const { class: cls, id, children } = this.props;    return (      <div id={id} data-slot="card-content" class={cx(this.$s.$content, cls)}>        {children}      </div>    );  }}/** * Tailwind's version conditions the top border/padding on a `.border-t` utility class being * present (`[.border-t]:pt-6`). There's no arbitrary-class selector equivalent in * `@praxisjs/css`, so pass a literal `class="kosmesis-bordered"` (instead of `border-t`) to opt * into the same spacing when you add a top border yourself. */@Component()export class CardFooter extends StatelessComponent<CardSlotProps> {  @Styled(CardStyles) $s!: CardStyles;  render() {    const { class: cls, id, children } = this.props;    return (      <div id={id} data-slot="card-footer" class={cx(this.$s.$footer, cls)}>        {children}      </div>    );  }}

Examples

Usage

import { Button } from "@/components/ui/button";
import {
  Card,
  CardAction,
  CardContent,
  CardDescription,
  CardFooter,
  CardHeader,
  CardTitle,
} from "@/components/ui/card";

<Card class="w-96">
  <CardHeader>
    <CardTitle>Create project</CardTitle>
    <CardDescription>Deploy your new project in one click.</CardDescription>
    <CardAction>
      <Button variant="ghost" size="sm">⋯</Button>
    </CardAction>
  </CardHeader>
  <CardContent>
    <p>Project settings go here.</p>
  </CardContent>
  <CardFooter class="border-t">
    <Button class="w-full">Deploy</Button>
  </CardFooter>
</Card>

Parts

Card, CardHeader, CardTitle, CardDescription, CardAction, CardContent, CardFooter — all accept class, id, and children. CardHeader uses a grid layout so CardAction (typically a menu button) sits in the top-right corner automatically when present.

On this page