Skip to content
  1. Keyline UI
  2. Resizable
Keyline UIComponents
  • Get started
    • Installation
    • Theming
    • Modes
    • Configuration
    • Composition
  • Actions
    • Button
    • Button group
    • Split button
    • Toggle
    • Toggle group
    • Toolbar
    • Menu
    • Menubar
    • Context menu
  • Inputs
    • Field and form
    • Checkbox
    • Checkbox group
    • Radio group
    • Switch
    • Slider
    • Segmented
    • Select
    • Native select
    • Combobox
    • Multiselect
    • Number field
    • OTP field
    • Time field
    • Date picker and calendar
    • Date range
  • Data
    • Table
    • Chart
    • Stat and meter
    • Status and badge
    • Progress
    • Skeleton
    • Empty
    • Avatar
    • Kbd
    • Marker
  • Navigation
    • Tabs
    • Breadcrumb
    • Pagination
    • Nav
    • Navigation menu
    • Command menu
  • Overlays
    • Dialog
    • Alert dialog
    • Drawer
    • Sheet
    • Popover
    • Hover card
    • Tooltip
    • Toast
    • Notice
  • Layout
    • App shell
    • Sidebar
    • Panel
    • Resizable
    • Scroll area
    • Accordion
    • Collapsible
    • Carousel
    • Inspector
    • Item
    • Direction
  • Conversation
    • Message
    • Bubble
    • Message scroller
    • Attachment
    • Questionnaire
  • Blocks
    • Shipments

Resizable

Panes that share one seam you can move. Hover or drag thickens it; arrow keys move it, Enter collapses a collapsible pane, and a double-click resets it. It draws no frame of its own, so put it in a Panel or a shell column.

Loading the live example…

"use client"

import { Phone } from "lucide-react"
import { useState } from "react"
import { Button } from "@/components/keyline/ui/button"
import { Item, ItemContent, ItemList, ItemMedia, ItemMeta } from "@/components/keyline/ui/item"
import { Panel, PanelBody, PanelHeader } from "@/components/keyline/ui/panel"
import { ResizableHandle, ResizablePanel, ResizablePanelGroup, usePanelRef } from "@/components/keyline/ui/resizable"

const CALLS = [
  { id: "c1", who: "Lackawanna County", when: "9:12", note: "Wants 40 more cases before the county fair. Dwight promised Thursday." },
  { id: "c2", who: "Blue Cross of Pennsylvania", when: "10:40", note: "Asked about the recycled stock. Jim is sending samples." },
  { id: "c3", who: "Vance Refrigeration", when: "13:05", note: "Nobody at the dock to sign. Bob says try again tomorrow at nine." },
]

const phone = <Phone size={16} aria-hidden="true" />

export function CallLog() {
  const [chosen, setChosen] = useState("c1")
  const [listHidden, setListHidden] = useState(false)
  const list = usePanelRef()
  const call = CALLS.find((c) => c.id === chosen)!

  return (
    <Panel>
      <PanelHeader
        title="Today's calls"
        actions={
          // dragging needs a pointer alternative: this button collapses and restores the list
          <Button layout="cell" onClick={() => (listHidden ? list.current?.expand() : list.current?.collapse())}>
            {listHidden ? "Show list" : "Hide list"}
          </Button>
        }
      />
      <PanelBody flush>
        {/* the group fills its parent's height, and a Panel body's rows size to their content: give it a box */}
        <div className="h-[340px]">
          <ResizablePanelGroup orientation="vertical">
            <ResizablePanel id="calls" panelRef={list} defaultSize="55%" minSize="35%" collapsible onResize={(s) => setListHidden(s.asPercentage === 0)}>
              {/* the pane is usually taller than three rows: close the last row so it doesn't dangle */}
              <ItemList layout="cell" aria-label="Today's calls" className="[&>:last-child]:border-b [&>:last-child]:border-kl-line">
                {CALLS.map((c) => (
                  <Item key={c.id} current={c.id === chosen} render={<button type="button" />} onClick={() => setChosen(c.id)}>
                    {/* the media cell is what floods for the current row */}
                    <ItemMedia>{phone}</ItemMedia>
                    <ItemContent title={c.who} />
                    <ItemMeta>{c.when}</ItemMeta>
                  </Item>
                ))}
              </ItemList>
            </ResizablePanel>
            <ResizableHandle withHandle aria-label="Call list height" />
            <ResizablePanel id="note" minSize="25%">
              <div className="grid gap-2 p-4 text-[14px]">
                <h3 className="m-0 text-[15px] font-semibold">{call.who}</h3>
                <p className="m-0 text-kl-ink-2">{call.note}</p>
              </div>
            </ResizablePanel>
          </ResizablePanelGroup>
        </div>
      </PanelBody>
    </Panel>
  )
}

Installation

npx shadcn@latest add @keyline-ui/resizable

Run it where your components.json is. The first time, map the @keyline-ui registry: one command, in Installation.

Usage

import { ResizableHandle, ResizablePanel, ResizablePanelGroup, usePanelRef } from "@/components/keyline/ui/resizable"
<ResizablePanelGroup orientation="vertical">
  <ResizablePanel defaultSize="55%" minSize="35%" collapsible>…</ResizablePanel>
  <ResizableHandle withHandle aria-label="Call list height" />
  <ResizablePanel minSize="25%">…</ResizablePanel>
</ResizablePanelGroup>

Props

PropTypeDefaultDescription
orientation"horizontal" | "vertical"Default "horizontal"On ResizablePanelGroup.
defaultSize · minSize · maxSizenumber | stringDefault —On ResizablePanel: numbers are pixels, strings percentages ("30%").
collapsiblebooleanDefault falseLets the pane fold away past its minimum.
panelRefusePanelRef()Default —collapse(), expand() and resize(), for a button that does what a drag does.
withHandlebooleanDefault falseOn ResizableHandle: a grip on the seam.
aria-labelstringDefault —On ResizableHandle: name it after the pane it resizes.

Dragging needs another way in (WCAG 2.5.7): the example's Hide list button collapses the pane. Built on react-resizable-panels; useDefaultLayout remembers the layout.

Sheet 60 of 73PreviousPanelNextScroll area
On this page