Agents (llms.txt)

Surface

A composable card-like surface with header and footer slots

Project status

A summary of recent activity across all open issues and pull requests.

Compose any content inside Surface.Content. Header, Action, and Footer are optional — use whichever parts the surface needs.
import { Button } from '@/components/button';
import { Surface } from '@/components/surface';

export default function SurfacePreview() {
  return (
    <div className="flex w-full max-w-md flex-col gap-6">
      <Surface>
        <Surface.Header>
          <Surface.Title>Project status</Surface.Title>
          <Surface.Description>
            A summary of recent activity across all open issues and pull
            requests.
          </Surface.Description>
          <Surface.Action>
            <Button variant="ghost" size="sm">
              View
            </Button>
          </Surface.Action>
        </Surface.Header>
        <Surface.Content minHeight="md">
          Compose any content inside Surface.Content. Header, Action, and Footer
          are optional — use whichever parts the surface needs.
        </Surface.Content>
        <Surface.Footer>
          <Button variant="outline" size="sm">
            Cancel
          </Button>
          <Button size="sm">Save</Button>
        </Surface.Footer>
      </Surface>
    </div>
  );
}

Dependencies

Source Code

import type { VariantProps } from 'cva';

import { Slot } from '@/components/slot';
import { cn, cva } from '@/lib/utils/classnames';

const surfaceStyle = cva({
  base: 'relative border border-border',
  variants: {
    /**
     * Role-based elevation, after Atlassian + Linear's luminance hierarchy.
     * Light mode pairs each step with a matching shadow; dark mode steps
     * the surface lightness instead (lighter = higher).
     */
    elevation: {
      sunken: 'border-transparent bg-surface-sunken shadow-none',
      // `default` keeps its border — in light mode it's white-on-white and
      // needs the edge. The luminance step alone covers dark mode.
      default: 'bg-surface-default shadow-none',
      // `raised` and `overlay` rely on the lightness step + paired shadow,
      // so the border becomes redundant noise — drop it.
      raised: 'border-transparent bg-surface-raised shadow-(--shadow-raised)',
      overlay:
        'border-transparent bg-surface-overlay shadow-(--shadow-overlay)',
    },
    radius: {
      none: 'rounded-none',
      sm: 'rounded-md',
      md: 'rounded-xl',
      lg: 'rounded-2xl',
      xl: 'rounded-3xl',
    },
    padding: {
      none: 'p-0 [--surface-pad:--spacing(0)]',
      sm: 'p-3 [--surface-pad:--spacing(3)]',
      md: 'p-5 [--surface-pad:--spacing(5)]',
      lg: 'p-6 [--surface-pad:--spacing(6)] md:p-8 md:[--surface-pad:--spacing(8)]',
      xl: 'p-8 [--surface-pad:--spacing(8)] md:p-12 md:[--surface-pad:--spacing(12)]',
    },
    interactive: {
      true: 'cursor-pointer transition-[transform,background-color] duration-(--duration-hover) ease-(--ease) hover:bg-foreground/2 active:scale-(--press-scale)',
      false: '',
    },
  },
  defaultVariants: {
    elevation: 'default',
    radius: 'md',
    padding: 'md',
    interactive: false,
  },
});

interface SurfaceProps
  extends React.ComponentPropsWithRef<'div'>,
    VariantProps<typeof surfaceStyle> {
  asChild?: boolean;
}

const Surface = ({
  ref,
  className,
  elevation,
  radius,
  padding,
  interactive,
  asChild,
  ...rest
}: SurfaceProps) => {
  const Comp = asChild ? Slot : 'div';
  return (
    <Comp
      ref={ref}
      className={cn(
        surfaceStyle({ elevation, radius, padding, interactive }),
        className
      )}
      {...rest}
    />
  );
};

/**
 * Header lays out Title + Description in a stack, with an optional Action
 * pinned to the right via grid placement. Mirrors shadcn's CardHeader API
 * but reads as a Foundations primitive.
 */
const SurfaceHeader = ({
  ref,
  className,
  ...rest
}: React.ComponentPropsWithRef<'div'>) => (
  <div
    ref={ref}
    className={cn(
      '-mx-(--surface-pad) -mt-(--surface-pad) mb-(--surface-pad) grid auto-rows-min grid-cols-[1fr_auto] items-start gap-x-4 gap-y-1 border-border border-b px-(--surface-pad) py-3.5',
      className
    )}
    {...rest}
  />
);

interface SurfaceTitleProps extends React.ComponentPropsWithRef<'h3'> {
  asChild?: boolean;
}

const SurfaceTitle = ({
  ref,
  className,
  asChild,
  ...rest
}: SurfaceTitleProps) => {
  const Comp = asChild ? Slot : 'h3';
  return (
    <Comp
      ref={ref}
      className={cn(
        'col-start-1 font-semibold text-base text-foreground leading-tight',
        className
      )}
      {...rest}
    />
  );
};

const SurfaceDescription = ({
  ref,
  className,
  ...rest
}: React.ComponentPropsWithRef<'p'>) => (
  <p
    ref={ref}
    className={cn(
      'col-start-1 text-foreground-secondary text-sm leading-snug',
      className
    )}
    {...rest}
  />
);

/**
 * Action sits at the right of the Header (column 2, spanning all rows) so it
 * stays vertically centered next to a Title-only header but pins to the top
 * when a Description is also present. Use for Buttons, Menu triggers, links.
 */
const SurfaceAction = ({
  ref,
  className,
  ...rest
}: React.ComponentPropsWithRef<'div'>) => (
  <div
    ref={ref}
    data-slot="surface-action"
    className={cn(
      'col-start-2 row-span-1 row-start-1 flex shrink-0 items-center gap-2 self-center',
      className
    )}
    {...rest}
  />
);

const surfaceContentStyle = cva({
  base: 'text-foreground text-sm leading-relaxed',
  variants: {
    /**
     * Reserves a baseline height for the body so sparse cards keep their shape
     * and rows of cards align cleanly. Steps map to common dashboard tile
     * heights — pick the one that matches the densest tile in the row.
     */
    minHeight: {
      none: '',
      sm: 'min-h-20',
      md: 'min-h-32',
      lg: 'min-h-48',
      xl: 'min-h-64',
    },
  },
  defaultVariants: {
    minHeight: 'none',
  },
});

interface SurfaceContentProps
  extends React.ComponentPropsWithRef<'div'>,
    VariantProps<typeof surfaceContentStyle> {}

/**
 * Content is the main body. It exists mainly for parity with the shadcn
 * vocabulary — without it, consumers had to know that a bare `<p>` works.
 * No padding of its own (Surface already pads); just resets prose styling
 * and (optionally) reserves a baseline height via `minHeight`.
 */
const SurfaceContent = ({
  ref,
  className,
  minHeight,
  ...rest
}: SurfaceContentProps) => (
  <div
    ref={ref}
    className={cn(surfaceContentStyle({ minHeight }), className)}
    {...rest}
  />
);

const SurfaceFooter = ({
  ref,
  className,
  ...rest
}: React.ComponentPropsWithRef<'div'>) => (
  <div
    ref={ref}
    className={cn(
      '-mx-(--surface-pad) mt-(--surface-pad) -mb-(--surface-pad) flex items-center justify-end gap-2 border-border border-t px-(--surface-pad) py-3',
      className
    )}
    {...rest}
  />
);

const CompoundSurface = Object.assign(Surface, {
  Header: SurfaceHeader,
  Title: SurfaceTitle,
  Description: SurfaceDescription,
  Action: SurfaceAction,
  Content: SurfaceContent,
  Footer: SurfaceFooter,
});

export type { SurfaceContentProps, SurfaceProps, SurfaceTitleProps };
export { CompoundSurface as Surface, surfaceStyle };

Server component. No 'use client' — elevation surface wrapper; cva + Slot only, no hooks or own handlers.

Surface is the canvas you build cards, panels, and dashboard tiles on top of. It is intentionally low-opinion: pick a variant, radius, and padding to fit the surrounding context.

Anatomy


          <Surface>
  <Surface.Header>
    <Surface.Title>Title</Surface.Title>
    <Surface.Description>Optional supporting copy</Surface.Description>
    <Surface.Action>{actionsOrMenu}</Surface.Action>
  </Surface.Header>
  <Surface.Content>{body}</Surface.Content>
  <Surface.Footer>{footerActions}</Surface.Footer>
</Surface>
        

Every part is optional — drop in only the slots you need. Surface.Action is positioned by Surface.Header (right column, vertically centered) so it sits correctly whether or not a Description is present.

API Reference

Surface

Extends the div element.

Prop Default Type
elevation "default" "sunken""default""raised""overlay"
radius "md" "none""sm""md""lg""xl"
padding "md" "none""sm""md""lg""xl"
interactive - boolean
asChild - boolean

elevation is role-based, after Atlassian and Linear:

  • sunken — page surface “well” under inset cards / panels
  • default — flat surfaces (the canvas), no shadow
  • raised — cards stacked on default; pairs with a subtle shadow
  • overlay — popovers, menus, nested cards; pairs with a stronger shadow

Light mode shows depth via paired shadows; dark mode shows depth via luminance (each step is ~3% L lighter than the one below it).

Surface.Header

Extends the div element. Lays Title, Description, and Action out in a 2-column grid: text stacks in the left column, Action pins to the right column. Sits flush against the surface edges via --surface-pad.

Surface.Title

Extends the h3 element. Use asChild to swap the heading level.

Prop Default Type
asChild - boolean

Surface.Description

Extends the p element.

Surface.Action

Extends the div element. Place inside Surface.Header next to the Title / Description.

Surface.Content

Extends the div element. The main body — exists so consumers don’t reach for a bare <p> or <div> and get unstyled prose. Pass minHeight to reserve a baseline height; useful for keeping a row of cards visually aligned when each has different amounts of content.

Prop Default Type
minHeight "none" "none""sm""md""lg""xl"

Surface.Footer

Extends the div element. Sits flush against the bottom edges via --surface-pad and is right-aligned for action buttons by default.

Examples

Default

Project status

A summary of recent activity across all open issues and pull requests.

Compose any content inside Surface.Content. Header, Action, and Footer are optional — use whichever parts the surface needs.
import { Button } from '@/components/button';
import { Surface } from '@/components/surface';

export default function SurfacePreview() {
  return (
    <div className="flex w-full max-w-md flex-col gap-6">
      <Surface>
        <Surface.Header>
          <Surface.Title>Project status</Surface.Title>
          <Surface.Description>
            A summary of recent activity across all open issues and pull
            requests.
          </Surface.Description>
          <Surface.Action>
            <Button variant="ghost" size="sm">
              View
            </Button>
          </Surface.Action>
        </Surface.Header>
        <Surface.Content minHeight="md">
          Compose any content inside Surface.Content. Header, Action, and Footer
          are optional — use whichever parts the surface needs.
        </Surface.Content>
        <Surface.Footer>
          <Button variant="outline" size="sm">
            Cancel
          </Button>
          <Button size="sm">Save</Button>
        </Surface.Footer>
      </Surface>
    </div>
  );
}

Previous

Stat

Next

Switch