Klarhimmel UI

Scroll area

Stable

Content that scrolls inside a fixed box: a named region, a tab stop, a thin scrollbar and a soft edge where there is more.

npx shadcn add @klarhimmel/scroll-area

When to use

  • Use for

    A list in a card, a code block or a strip that must not push the page.

  • Not for

    Tables. Table scrolls itself. A whole page. Let the page scroll.

  • Pairs with

    Card, Popover, code.

Preview

  • Room 201: Garden, 1190 kr
  • Room 202: Seaside, 1340 kr
  • Room 203: Loft, 1490 kr
  • Room 204: Courtyard, 1640 kr
  • Room 205: Garden, 1790 kr
  • Room 206: Seaside, 1190 kr
  • Room 207: Loft, 1340 kr
  • Room 208: Courtyard, 1490 kr
  • Room 209: Garden, 1640 kr
  • Room 210: Seaside, 1790 kr
  • Room 211: Loft, 1190 kr
  • Room 212: Courtyard, 1340 kr
  • Room 213: Garden, 1490 kr
  • Room 214: Seaside, 1640 kr
  • Room 215: Loft, 1790 kr
  • Room 216: Courtyard, 1190 kr
  • Room 217: Garden, 1340 kr
  • Room 218: Seaside, 1490 kr
  • Room 219: Loft, 1640 kr
  • Room 220: Courtyard, 1790 kr
  • Room 221: Garden, 1190 kr
  • Room 222: Seaside, 1340 kr
  • Room 223: Loft, 1490 kr
  • Room 224: Courtyard, 1640 kr
scroll-area.tsx
import { Theme } from '@/components/klarhimmel/theme/style';
import { ScrollArea } from '@/components/klarhimmel/ui/scroll-area';

export default function Example() {
  return (
    <Theme theme="brand">
      <ScrollArea aria-label="Rooms and rates" className="max-h-64">
        <ul>
          <li>Room 201, Garden, 1 190 kr</li>
          <li>Room 202, Seaside, 1 340 kr</li>
          <li>Room 203, Loft, 1 490 kr</li>
        </ul>
      </ScrollArea>
    </Theme>
  );
}

Install

Import

import { ScrollArea } from '@/components/klarhimmel/ui/scroll-area';
Lands in
components/klarhimmel/ui/scroll-area.tsx, plus cn() and the theme item if they are missing. Import theme.css after Tailwind.
Render in
Render it inside <Theme>, once near the root of your app. Tokens and fonts are scoped to it, so outside it the component is unstyled.

Examples

  • Horizontal

    • Mon 16 NovFull
    • Tue 17 Nov5 rooms free
    • Wed 18 Nov6 rooms free
    • Thu 19 NovFull
    • Fri 20 Nov4 rooms free
    • Sat 21 Nov5 rooms free
    • Sun 22 NovFull
    • Mon 23 Nov7 rooms free
    • Tue 24 Nov4 rooms free
    • Wed 25 NovFull
    import { ScrollArea } from '@/components/klarhimmel/ui/scroll-area';
    
    const days = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun', 'Mon', 'Tue', 'Wed'];
    
    export default function ScrollAreaHorizontal() {
      return (
        <div className="w-[min(28rem,calc(100cqw-3rem))]">
          <ScrollArea aria-label="Next ten days" orientation="horizontal" className="pb-3">
            <ul className="flex gap-3">
              {days.map((d, i) => (
                <li key={i} className="flex h-24 w-28 shrink-0 flex-col justify-between rounded-kh-lg border border-kh-line bg-kh-surface p-3 text-kh-small">
                  <span className="font-medium">{d} {16 + i} Nov</span>
                  <span className="text-kh-muted">{i % 3 === 0 ? 'Full' : `${4 + (i % 4)} rooms free`}</span>
                </li>
              ))}
            </ul>
          </ScrollArea>
        </div>
      );
    }
  • Code

    {
      "booking": "4821",
      "room": "204",
      "guests": [
        { "name": "Maja Sund", "arrival": "2026-11-14", "nights": 2, "note": "Late check-in, travel cot needed, quiet room away from the lift" },
        { "name": "Oskar Lind", "arrival": "2026-11-14", "nights": 2, "note": "Allergic to feathers: synthetic bedding" }
      ],
      "rate": { "code": "REFUNDABLE", "total": 2380, "currency": "SEK", "refundable_until": "2026-11-07" }
    }
    import { ScrollArea } from '@/components/klarhimmel/ui/scroll-area';
    
    const code = `{
      "booking": "4821",
      "room": "204",
      "guests": [
        { "name": "Maja Sund", "arrival": "2026-11-14", "nights": 2, "note": "Late check-in, travel cot needed, quiet room away from the lift" },
        { "name": "Oskar Lind", "arrival": "2026-11-14", "nights": 2, "note": "Allergic to feathers: synthetic bedding" }
      ],
      "rate": { "code": "REFUNDABLE", "total": 2380, "currency": "SEK", "refundable_until": "2026-11-07" }
    }`;
    
    export default function ScrollAreaCode() {
      return (
        <div className="w-[min(28rem,calc(100cqw-3rem))] rounded-kh-lg bg-kh-surface-2 p-2">
          <ScrollArea aria-label="Booking payload" orientation="both" className="max-h-48">
            <pre className="px-2 py-2 font-kh-mono text-kh-caption leading-relaxed text-kh-ink">{code}</pre>
          </ScrollArea>
        </div>
      );
    }

API reference

PropTypeDefaultDescription
aria-labelstring-Required: names the region, which keyboard users reach with Tab.
orientation'vertical' | 'horizontal' | 'both''vertical'Which way it scrolls. Set the height or width with a class such as max-h-64.
classNamestring-Extra classes, merged after the component's own.

Accessibility

KeyDoes
TabReaches the region
Arrow keysScroll by a line
Page Up / Page Down / SpaceScroll by a page
Home / EndJump to the start or the end

The region is named and focusable, so a keyboard can scroll it. The scrollbar is thin but always drawn where the platform allows, and a soft edge shows where there is more.

Accessibility