# Corner Frame

A box with bracket marks on its corners, in any size, thickness, colour and inset.

Docs: https://corsairui.vercel.app/docs/corner-frame

## Install

```bash
pnpm dlx shadcn@latest add @corsair-ui/corner-frame
```

Also adds: `utils`.
npm: `@radix-ui/react-slot`, `clsx`, `tailwind-merge`.

## API

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `corners` | `"all" \| ("top-left" \| "top-right" \| "bottom-left" \| "bottom-right")[]` | `"all"` | Which corners get a bracket. |
| `size / thickness / inset` | `string` | `"12px" / "2px" / "0px"` | CSS lengths; a negative inset sits outside the box. |
| `color` | `string` | `var(--primary)` | Sets --corner-frame-color. |
| `asChild` | `boolean` | `false` | Frame your own element. |

## Styling

Restyle it in the project that installs it: `className` on each part, the theme variables, and these attributes with arbitrary variants (`[&_[data-slot=…]]:…`, `data-[state=…]:…`).

data-slot: `corner-frame`, `corner-frame-corner`

State attributes:

- `data-corner` (computed)

## Accessibility

- The brackets are decorative and hidden from screen readers.

## Examples

On the docs page: Corners, size, colour and inset. The usage examples in the source comments below are the reference.

## Source

What `shadcn add` writes into the project:

### corner-frame.tsx

```tsx
import { Slot, Slottable } from "@radix-ui/react-slot";
import type { ComponentProps, CSSProperties } from "react";
import { cn } from "@/registry/default/lib/utils";

type CornerFrameCorner = "top-left" | "top-right" | "bottom-left" | "bottom-right";

const CORNERS: CornerFrameCorner[] = ["top-left", "top-right", "bottom-left", "bottom-right"];

const LINE = "var(--corner-frame-thickness)";
const INSET = "var(--corner-frame-inset)";

/** Where each bracket sits and which two edges it draws. */
const CORNER_STYLES: Record<CornerFrameCorner, CSSProperties> = {
  "top-left": { top: INSET, left: INSET, borderTopWidth: LINE, borderLeftWidth: LINE },
  "top-right": { top: INSET, right: INSET, borderTopWidth: LINE, borderRightWidth: LINE },
  "bottom-left": { bottom: INSET, left: INSET, borderBottomWidth: LINE, borderLeftWidth: LINE },
  "bottom-right": { bottom: INSET, right: INSET, borderBottomWidth: LINE, borderRightWidth: LINE },
};

interface CornerFrameProps extends Omit<ComponentProps<"div">, "color"> {
  /** Which corners get a bracket: "all", or a list of them. */
  corners?: "all" | CornerFrameCorner[];
  /** Length of each arm of a bracket. Any CSS length. */
  size?: string;
  /** Line width of the brackets. Any CSS length. */
  thickness?: string;
  /**
   * Bracket colour: any CSS colour or theme variable. Sets
   * `--corner-frame-color`; without it the brackets use that variable when an
   * ancestor sets it, and `var(--primary)` otherwise.
   */
  color?: string;
  /** How far the brackets sit inside the edges. Negative values move them outside. */
  inset?: string;
  /** Render the single child element as the frame instead of a `<div>`. */
  asChild?: boolean;
}

/**
 * A box with bracket marks on its corners, like a viewfinder. The brackets
 * are decoration: they are hidden from screen readers and ignore the
 * pointer, so the content inside works as usual. It renders no motion and
 * needs no JavaScript.
 *
 * @example
 * <CornerFrame corners={["top-left", "bottom-right"]} size="16px" className="p-6">
 *   <p>Framed content</p>
 * </CornerFrame>
 */
function CornerFrame({
  corners = "all",
  size = "12px",
  thickness = "2px",
  color,
  inset = "0px",
  asChild = false,
  className,
  style,
  children,
  ...props
}: CornerFrameProps) {
  const Comp = asChild ? Slot : "div";
  const shown = corners === "all" ? CORNERS : CORNERS.filter((corner) => corners.includes(corner));

  return (
    <Comp
      data-slot="corner-frame"
      className={cn("relative", className)}
      style={
        {
          "--corner-frame-size": size,
          "--corner-frame-thickness": thickness,
          "--corner-frame-inset": inset,
          ...(color ? { "--corner-frame-color": color } : null),
          ...style,
        } as CSSProperties
      }
      {...props}
    >
      <Slottable>{children}</Slottable>
      {shown.map((corner) => (
        <span
          key={corner}
          aria-hidden="true"
          data-slot="corner-frame-corner"
          data-corner={corner}
          className="pointer-events-none absolute border-0 border-solid"
          style={{
            width: "var(--corner-frame-size)",
            height: "var(--corner-frame-size)",
            borderColor: "var(--corner-frame-color, var(--primary))",
            ...CORNER_STYLES[corner],
          }}
        />
      ))}
    </Comp>
  );
}

export { CornerFrame, type CornerFrameCorner, type CornerFrameProps };
```
