Panel
A layered surface with a header, title, actions, content and footer.
Harbor
Last played on May 15
A survival world with three players and a very large lighthouse.
Installation
npx shadcn@latest add @runqora/craft-panelUsage
import {
Panel,
PanelContent,
PanelHeader,
PanelTitle,
} from "@/components/craft/panel";
<Panel>
<PanelHeader>
<PanelTitle>Harbor</PanelTitle>
</PanelHeader>
<PanelContent>A survival world.</PanelContent>
</Panel>;Every part is optional, so a panel can be only PanelContent. It has no client
code, so it works in React Server Components with no boundary.
Examples
Variants
surface
A panel with the surface surface.
raised
A panel with the raised surface.
sunken
A panel with the sunken surface.
light
A panel with the light surface.
Props
Prop
Type
Each part also accepts every attribute of the element it renders: <div> for
most parts, <h3> for PanelTitle and <p> for PanelDescription.
Accessibility
PanelTitlerenders an<h3>. If that doesn't fit your page's heading order, render your own heading insidePanelHeader.- A panel has no role of its own. When it's a landmark on your page, add
roleand a label, or wrap it in a<section>.
Customization
Panels read their padding from --craft-space, on any level you set it.
<Panel className="[--craft-space:2rem]">...</Panel>Dependencies
class-variance-authority- shadcn's
utils, andcraft-theme
Source
import { cva, type VariantProps } from "class-variance-authority";
import type { ComponentProps } from "react";
import { cn } from "@/lib/utils";
const panelVariants = cva(
"craft-bevel text-craft-text flex flex-col [--bevel-depth:0px] [--bevel-edge:transparent]",
{
defaultVariants: { variant: "surface" },
variants: {
variant: {
/** The light panels of Ore UI, in both color modes. */
light:
"text-craft-secondary-fg [--bevel-face:var(--craft-secondary)] [--bevel-hi:var(--craft-secondary-hi)] [--bevel-lo:var(--craft-secondary-lo)] [--craft-text-muted:var(--craft-disabled-fg)]",
/** Lifted from the page, like a tab or dialog header. */
raised:
"[--bevel-face:var(--craft-raised)] [--bevel-hi:var(--craft-raised-hi)] [--bevel-lo:var(--craft-raised-lo)]",
/** Recessed into the page, like a dialog body. */
sunken:
"[--bevel-face:var(--craft-sunken)] [--bevel-hi:var(--craft-sunken-hi)] [--bevel-lo:var(--craft-sunken-lo)]",
/** The everyday panel. */
surface:
"[--bevel-face:var(--craft-surface)] [--bevel-hi:var(--craft-surface-hi)] [--bevel-lo:var(--craft-surface-lo)]",
},
},
}
);
export interface PanelProps
extends ComponentProps<"div">,
VariantProps<typeof panelVariants> {}
export function Panel({ className, variant, ...props }: PanelProps) {
return (
<div
className={cn(panelVariants({ variant }), className)}
data-slot="panel"
{...props}
/>
);
}
export function PanelHeader({ className, ...props }: ComponentProps<"div">) {
return (
<div
className={cn(
"flex flex-wrap items-center gap-x-4 gap-y-1 p-(--craft-space)",
className
)}
data-slot="panel-header"
{...props}
/>
);
}
export function PanelTitle({ className, ...props }: ComponentProps<"h3">) {
return (
<h3
className={cn("font-craft-display text-2xl leading-tight", className)}
data-slot="panel-title"
{...props}
/>
);
}
export function PanelDescription({ className, ...props }: ComponentProps<"p">) {
return (
<p
className={cn(
"font-craft-body text-craft-text-muted w-full text-sm",
className
)}
data-slot="panel-description"
{...props}
/>
);
}
/** Controls at the end of a header. They follow the title on the same row. */
export function PanelActions({ className, ...props }: ComponentProps<"div">) {
return (
<div
className={cn("ml-auto flex items-center gap-2", className)}
data-slot="panel-actions"
{...props}
/>
);
}
export function PanelContent({ className, ...props }: ComponentProps<"div">) {
return (
<div
className={cn(
"font-craft-body px-(--craft-space) pb-(--craft-space) first:pt-(--craft-space)",
className
)}
data-slot="panel-content"
{...props}
/>
);
}
export function PanelFooter({ className, ...props }: ComponentProps<"div">) {
return (
<div
className={cn(
"mt-auto flex flex-wrap items-center gap-2 p-(--craft-space) pt-0",
className
)}
data-slot="panel-footer"
{...props}
/>
);
}
export { panelVariants };