shadimage
nteract
componentcell-containersource: ok · hit

CellContainer

A focus and selection wrapper for notebook cells with gutter ribbon system. Color-coded ribbons indicate cell type, with action area for play button and execution count.

npx shadcn add "https://nteract-elements.vercel.app/r/cell-container.json"
free

Visual style

terminal-mono

Interaction

dropdown

Tags auto-generated by rules on 9/24/2026 — empty means no evidence, not lack of support.

Files2 · source cached 10/14/2026, 3:37:07 PM

registry/cell/CellContainer.tsx
import { forwardRef, type ReactNode } from "react";
import { cn } from "@/lib/utils";
import { type GutterColorConfig, getGutterColors } from "./gutter-colors";

interface CellContainerProps {
  id: string;
  cellType: string;
  isFocused?: boolean;
  onFocus?: () => void;
  /** Content for the code/editor section (use with outputContent for segmented ribbon) */
  codeContent?: ReactNode;
  /** Content for the output section (renders with a different ribbon color) */
  outputContent?: ReactNode;
  /** Hide the output section (useful for preloading content invisibly) */
  hideOutput?: boolean;
  /** Legacy children prop - use codeContent/outputContent for segmented ribbon support */
  children?: ReactNode;
  /** Content to render in the left gutter action area (e.g., play button, execution count) */
  gutterContent?: ReactNode;
  /** Content to render in the right margin (e.g., cell controls, kebab menu) */
  rightGutterContent?: ReactNode;
  /** Custom color configuration for cell types not in defaults */
  customGutterColors?: Record<string, GutterColorConfig>;
  onDragStart?: (e: React.DragEvent) => void;
  onDragOver?: (e: React.DragEvent) => void;
  onDrop?: (e: React.DragEvent) => void;
  className?: string;
}

export const CellContainer = forwardRef<HTMLDivElement, CellContainerProps>(
  (
    {
      id,
      cellType,
      isFocused = false,
      onFocus,
      codeContent,
      outputContent,
      hideOutput,
      children,
      gutterContent,
      rightGutterContent,
      customGutterColors,
      onDragStart,
      onDragOver,
      onDrop,
      className,
    },
    ref,
  ) => {
    const colors = getGutterColors(cellType, customGutterColors);
    const ribbonColor = isFocused
      ? colors.ribbon.focused
      : colors.ribbon.default;
    const outputRibbonColor = isFocused
      ? colors.outputRibbon.focused
      : colors.outputRibbon.default;
    const bgColor = isFocused ? colors.background.focused : undefined;

    // Use segmented ribbon when codeContent is provided
    const useSegmentedRibbon = codeContent !== undefined;
    const hasOutput = outputContent !== undefined && outputContent !== null;

    return (
      <div
        ref={ref}
        data-slot="cell-container"
        data-cell-id={id}
        data-cell-type={cellType}
        className={cn(
          "cell-container group flex transition-colors duration-150",
          bgColor,
          isFocused && "-mx-16 px-16",
          className,
        )}
        onMouseDown={onFocus}
        draggable={!!onDragStart}
        onDragStart={onDragStart}
        onDragOver={onDragOver}
        onDrop={onDrop}
      >
        {/* Gutter area - action content only (ribbon moves to content rows for segmented) */}
        <div className="flex w-10 flex-shrink-0 flex-col items-end justify-start gap-0.5 pr-1 pt-3">
          {gutterContent}
        </div>
        {/* Cell content with ribbon */}
        {useSegmentedRibbon ? (
          <div className="flex min-w-0 flex-1 flex-col">
            {/* Code row - ribbon + content together so heights match */}
            <div className="flex">
              <div
                className={cn(
                  "w-1 transition-colors duration-150",
                  ribbonColor,
                )}
              />
              <div className="min-w-0 flex-1 py-3 pl-6 pr-3">{codeContent}</div>
            </div>
            {/* Output row - ribbon + content together */}
            {hasOutput && (
              <div className={cn("flex", hideOutput && "hidden")}>
                <div
                  className={cn(
                    "w-1 transition-colors duration-150",
                    outputRibbonColor,
                  )}
                />
                <div
                  className={cn(
                    "min-w-0 flex-1 py-2 pl-6 pr-3 transition-opacity duration-150",
                    !isFocused && "opacity-70",
                  )}
                >
                  {outputContent}
                </div>
              </div>
            )}
          </div>
        ) : (
          /* Legacy layout - ribbon + content side by side */
          <div className="flex min-w-0 flex-1">
            <div
              className={cn(
                "w-1 self-stretch transition-colors duration-150",
                ribbonColor,
              )}
            />
            <div className="min-w-0 flex-1 py-3 pl-6 pr-3">{children}</div>
          </div>
        )}
        {/* Right margin - pt-3 aligns with left gutter, appears on hover/focus */}
        {rightGutterContent && (
          <div
            className={cn(
              "flex w-10 flex-shrink-0 flex-col items-center gap-1 pt-3",
              "opacity-100 transition-opacity duration-150",
              "sm:opacity-0 sm:group-hover:opacity-100 sm:focus-within:opacity-100",
              isFocused && "sm:opacity-100",
            )}
          >
            {rightGutterContent}
          </div>
        )}
      </div>
    );
  },
);

CellContainer.displayName = "CellContainer";
registry/cell/gutter-colors.ts
export interface GutterColorConfig {
  ribbon: {
    default: string;
    focused: string;
  };
  outputRibbon: {
    default: string;
    focused: string;
  };
  background: {
    focused: string;
  };
}

/**
 * Default gutter colors for built-in cell types.
 * Colors are designed to match the existing CellTypeButton color scheme.
 */
export const defaultGutterColors: Record<string, GutterColorConfig> = {
  code: {
    ribbon: {
      default: "bg-gray-200 dark:bg-gray-700",
      focused: "bg-sky-400 dark:bg-sky-600",
    },
    outputRibbon: {
      default:
        "bg-gradient-to-b from-gray-200/60 to-gray-200 dark:from-gray-700/60 dark:to-gray-700",
      focused:
        "bg-gradient-to-b from-sky-400/60 to-sky-400 dark:from-sky-600/60 dark:to-sky-600",
    },
    background: {
      focused: "bg-sky-50/20 dark:bg-sky-900/10",
    },
  },
  markdown: {
    ribbon: {
      default: "bg-gray-200 dark:bg-gray-700",
      focused: "bg-emerald-400 dark:bg-emerald-600",
    },
    outputRibbon: {
      default:
        "bg-gradient-to-b from-gray-200/60 to-gray-200 dark:from-gray-700/60 dark:to-gray-700",
      focused:
        "bg-gradient-to-b from-emerald-400/60 to-emerald-400 dark:from-emerald-600/60 dark:to-emerald-600",
    },
    background: {
      focused: "bg-emerald-50/20 dark:bg-emerald-900/10",
    },
  },
  sql: {
    ribbon: {
      default: "bg-amber-200 dark:bg-amber-800",
      focused: "bg-amber-400 dark:bg-amber-600",
    },
    outputRibbon: {
      default:
        "bg-gradient-to-b from-amber-200/60 to-amber-200 dark:from-amber-800/60 dark:to-amber-800",
      focused:
        "bg-gradient-to-b from-amber-400/60 to-amber-400 dark:from-amber-600/60 dark:to-amber-600",
    },
    background: {
      focused: "bg-amber-50/20 dark:bg-amber-900/10",
    },
  },
  ai: {
    ribbon: {
      default: "bg-purple-200 dark:bg-purple-800",
      focused: "bg-purple-400 dark:bg-purple-600",
    },
    outputRibbon: {
      default:
        "bg-gradient-to-b from-purple-200/60 to-purple-200 dark:from-purple-800/60 dark:to-purple-800",
      focused:
        "bg-gradient-to-b from-purple-400/60 to-purple-400 dark:from-purple-600/60 dark:to-purple-600",
    },
    background: {
      focused: "bg-purple-50/20 dark:bg-purple-900/10",
    },
  },
  raw: {
    ribbon: {
      default: "bg-rose-200 dark:bg-rose-800",
      focused: "bg-rose-400 dark:bg-rose-600",
    },
    outputRibbon: {
      default:
        "bg-gradient-to-b from-rose-200/60 to-rose-200 dark:from-rose-800/60 dark:to-rose-800",
      focused:
        "bg-gradient-to-b from-rose-400/60 to-rose-400 dark:from-rose-600/60 dark:to-rose-600",
    },
    background: {
      focused: "bg-rose-50/20 dark:bg-rose-900/10",
    },
  },
};

/**
 * Fallback colors for unknown cell types.
 * Uses neutral gray styling.
 */
export const fallbackGutterColors: GutterColorConfig = {
  ribbon: {
    default: "bg-gray-200 dark:bg-gray-700",
    focused: "bg-gray-400 dark:bg-gray-500",
  },
  outputRibbon: {
    default:
      "bg-gradient-to-b from-gray-200/60 to-gray-200 dark:from-gray-700/60 dark:to-gray-700",
    focused:
      "bg-gradient-to-b from-gray-400/60 to-gray-400 dark:from-gray-500/60 dark:to-gray-500",
  },
  background: {
    focused: "bg-gray-50/50 dark:bg-gray-900/30",
  },
};

/**
 * Get gutter colors for a cell type.
 * Falls back to neutral gray for unknown types.
 *
 * @param cellType - The cell type to get colors for
 * @param customColors - Optional custom color overrides
 */
export function getGutterColors(
  cellType: string,
  customColors?: Record<string, GutterColorConfig>,
): GutterColorConfig {
  // Check custom colors first
  if (customColors?.[cellType]) {
    return customColors[cellType];
  }
  // Then check defaults
  if (defaultGutterColors[cellType]) {
    return defaultGutterColors[cellType];
  }
  // Fall back to neutral
  return fallbackGutterColors;
}

More from nteract

Browse all components in this library →