shadimage
niko-table
componentdata-table-asidesource: ok · hit

DataTable Aside

Sidebar panel component for displaying row details or additional content alongside the table.

npx shadcn add "https://niko-table.com/r/data-table-aside.json"
free

Where it goes

sidebardashboard-shelldata-table

Use cases

dashboard

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

Dependencies

Bundle weight · light
iconslucide-react

Registry dependencies

https://niko-table.com/r/data-table-core.json

Files1 · source cached 10/9/2026, 4:50:02 AM

src/components/niko-table/components/data-table-aside.tsx
"use client"

/**
 * niko-table — created by Semir N. (Semkoo, https://github.com/Semkoo) with AI assistance.
 *
 * Before reporting anything: please check the changelog first.
 *  - In-repo: ./CHANGELOG.md
 *  - Docs site: https://niko-table.com/changelog
 *
 * Found a bug or have a fix? Open an issue or PR on GitHub so other
 * users (and future LLMs reading this code) benefit:
 * https://github.com/Semkoo/niko-table-registry
 */
import * as React from "react"
import { XIcon } from "lucide-react"
import { cn } from "@/lib/utils"

interface DataTableAsideContextValue {
  open: boolean
  onOpenChange: (open: boolean) => void
  side: "left" | "right"
}

const DataTableAsideContext = React.createContext<
  DataTableAsideContextValue | undefined
>(undefined)

function useDataTableAside() {
  const context = React.useContext(DataTableAsideContext)
  if (!context) {
    throw new Error(
      "DataTableAside components must be used within DataTableAside",
    )
  }
  return context
}

interface DataTableAsideProps {
  children: React.ReactNode
  side?: "left" | "right"
  open?: boolean
  onOpenChange?: (open: boolean) => void
  defaultOpen?: boolean
}

function DataTableAside({
  children,
  side = "right",
  open: controlledOpen,
  onOpenChange: controlledOnOpenChange,
  defaultOpen = false,
}: DataTableAsideProps) {
  const [internalOpen, setInternalOpen] = React.useState(defaultOpen)

  const open = controlledOpen ?? internalOpen
  const onOpenChange = controlledOnOpenChange ?? setInternalOpen

  /**
   * PERFORMANCE: Memoize context value to prevent unnecessary consumer re-renders
   *
   * WHY: Without memoization, a new context value object is created on every render.
   * React Context uses Object.is() to compare values - new object = all consumers re-render.
   *
   * IMPACT: Prevents unnecessary re-renders of DataTableAsideTrigger, DataTableAsideContent, etc.
   * when aside state hasn't changed.
   *
   * WHAT: Only creates new context value when open, onOpenChange, or side actually change.
   */
  const contextValue = React.useMemo<DataTableAsideContextValue>(
    () => ({
      open,
      onOpenChange,
      side,
    }),
    [open, onOpenChange, side],
  )

  return (
    <DataTableAsideContext.Provider value={contextValue}>
      {children}
    </DataTableAsideContext.Provider>
  )
}

DataTableAside.displayName = "DataTableAside"

interface DataTableAsideTriggerProps extends React.ComponentPropsWithoutRef<"button"> {
  asChild?: boolean
  children?: React.ReactNode
}

function DataTableAsideTrigger({
  className,
  asChild = false,
  children,
  ...props
}: DataTableAsideTriggerProps) {
  const { open, onOpenChange } = useDataTableAside()

  const handleToggle = React.useCallback(() => {
    onOpenChange(!open)
  }, [onOpenChange, open])

  if (asChild && React.isValidElement(children)) {
    const childProps = children.props as {
      onClick?: (e: React.MouseEvent) => void
    }
    return React.cloneElement(children, {
      onClick: (e: React.MouseEvent) => {
        handleToggle()
        childProps.onClick?.(e)
      },
    } as Partial<unknown> & React.Attributes)
  }

  return (
    <button
      data-slot="aside-trigger"
      type="button"
      className={className}
      onClick={handleToggle}
      {...props}
    >
      {children}
    </button>
  )
}

DataTableAsideTrigger.displayName = "DataTableAsideTrigger"

interface DataTableAsideContentProps extends React.ComponentPropsWithoutRef<"aside"> {
  width?: string
  sticky?: boolean
}

function DataTableAsideContent({
  children,
  className,
  width = "w-1/2",
  sticky = false,
  ...props
}: DataTableAsideContentProps) {
  const { open, side } = useDataTableAside()

  if (!open) return null

  const slideAnimation =
    side === "left" ? "slide-in-from-left" : "slide-in-from-right"

  return (
    <aside
      data-slot="aside-content"
      className={cn(
        "shrink-0 animate-in",
        width,
        slideAnimation,
        sticky && "sticky top-0",
        className,
      )}
      {...props}
    >
      {children}
    </aside>
  )
}

DataTableAsideContent.displayName = "DataTableAsideContent"

function DataTableAsideHeader({
  className,
  ...props
}: React.ComponentPropsWithoutRef<"div">) {
  return (
    <div
      data-slot="aside-header"
      className={cn("flex flex-col gap-2", className)}
      {...props}
    />
  )
}

DataTableAsideHeader.displayName = "DataTableAsideHeader"

function DataTableAsideTitle({
  className,
  ...props
}: React.ComponentPropsWithoutRef<"h3">) {
  return (
    <h3
      data-slot="aside-title"
      className={cn("text-lg leading-none font-semibold", className)}
      {...props}
    />
  )
}

DataTableAsideTitle.displayName = "DataTableAsideTitle"

function DataTableAsideDescription({
  className,
  ...props
}: React.ComponentPropsWithoutRef<"p">) {
  return (
    <p
      data-slot="aside-description"
      className={cn("text-sm text-muted-foreground", className)}
      {...props}
    />
  )
}

DataTableAsideDescription.displayName = "DataTableAsideDescription"

interface DataTableAsideCloseProps extends React.ComponentPropsWithoutRef<"button"> {
  showIcon?: boolean
}

function DataTableAsideClose({
  className,
  showIcon = true,
  children,
  ...props
}: DataTableAsideCloseProps) {
  const { onOpenChange } = useDataTableAside()

  const handleClose = React.useCallback(() => {
    onOpenChange(false)
  }, [onOpenChange])

  return (
    <button
      data-slot="aside-close"
      type="button"
      className={cn(
        "rounded-xs opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:ring-2 focus:ring-ring focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none",
        className,
      )}
      onClick={handleClose}
      {...props}
    >
      {showIcon && <XIcon className="size-4" />}
      {children}
      {showIcon && !children && <span className="sr-only">Close</span>}
    </button>
  )
}

DataTableAsideClose.displayName = "DataTableAsideClose"

export {
  DataTableAside,
  DataTableAsideTrigger,
  DataTableAsideContent,
  DataTableAsideHeader,
  DataTableAsideTitle,
  DataTableAsideDescription,
  DataTableAsideClose,
}

More from niko-table

Browse all components in this library →