JOYCO Registry
Components

Scroll Area

A scrollable area component with customizable top and bottom shadows that appear when content overflows.

Demo

New comment

Sarah left a comment on your post

New follower

Alex started following you

Payment received

You received $250.00 from Client Co.

Reminder

Team standup meeting in 30 minutes

New message

Jordan sent you a direct message

Event tomorrow

Product launch scheduled for 9:00 AM

New comment

Sarah left a comment on your post

New follower

Alex started following you

'use client'

import * as ScrollArea from '@/registry/joyco/blocks/scroll-area'
import { useState, useRef, useEffect } from 'react'
import { Button } from '../components/ui/button'
import {
  Plus,
  X,
  MessageSquare,
  UserPlus,
  CreditCard,
  Bell,
  Mail,
  Calendar,
} from 'lucide-react'
import { cn } from '@/lib/utils'

const notifications = [
  {
    icon: MessageSquare,
    title: 'New comment',
    description: 'Sarah left a comment on your post',
  },
  {
    icon: UserPlus,
    title: 'New follower',
    description: 'Alex started following you',
  },
  {
    icon: CreditCard,
    title: 'Payment received',
    description: 'You received $250.00 from Client Co.',
  },
  {
    icon: Bell,
    title: 'Reminder',
    description: 'Team standup meeting in 30 minutes',
  },
  {
    icon: Mail,
    title: 'New message',
    description: 'Jordan sent you a direct message',
  },
  {
    icon: Calendar,
    title: 'Event tomorrow',
    description: 'Product launch scheduled for 9:00 AM',
  },
]

function ScrollAreaDemo() {
  const [items, setItems] = useState<number[]>([0, 1, 2, 3, 4, 5, 6, 7])
  const nextIdRef = useRef(4)
  const scrollRef = useRef<HTMLDivElement>(null)

  useEffect(() => {
    if (scrollRef.current) {
      scrollRef.current.scrollTo({
        top: scrollRef.current.scrollHeight,
        behavior: 'smooth',
      })
    }
  }, [items.length])

  const addNotification = () => {
    setItems((prev) => [...prev, nextIdRef.current++])
  }

  const dismissNotification = (id: number) => {
    setItems((prev) => prev.filter((item) => item !== id))
  }

  return (
    <div className="mx-auto w-full max-w-md p-10">
      <ScrollArea.Root
        className="h-100 w-full"
        topShadowGradient="bg-linear-to-b from-card to-transparent"
        bottomShadowGradient="bg-linear-to-t from-card to-transparent"
      >
        <ScrollArea.Content ref={scrollRef} className="fancy-scroll space-y-2">
          {items.length === 0 ? (
            <div className="flex h-48 flex-col items-center justify-center text-center">
              <div className="bg-muted mb-3 rounded-full p-3">
                <Bell className="text-muted-foreground h-6 w-6" />
              </div>
              <p className="text-muted-foreground text-sm">No notifications</p>
            </div>
          ) : (
            items.map((id, index) => {
              const notification = notifications[id % notifications.length]
              const Icon = notification.icon
              return (
                <div
                  key={`${id}-${index}`}
                  className="bg-background rounded-lg border p-3"
                >
                  <div className="flex items-start gap-3">
                    <div className={cn('bg-muted rounded-sm p-2')}>
                      <Icon className={cn('text-muted-foreground h-5 w-5')} />
                    </div>
                    <div className="min-w-0 flex-1">
                      <h3 className="font-medium">{notification.title}</h3>
                      <p className="text-muted-foreground mt-0.5 text-xs">
                        {notification.description}
                      </p>
                    </div>
                    <button
                      onClick={() => dismissNotification(id)}
                      className="text-muted-foreground hover:text-foreground -mt-1 -mr-1 rounded p-1 transition-colors"
                    >
                      <X className="h-4 w-4" />
                    </button>
                  </div>
                </div>
              )
            })
          )}
        </ScrollArea.Content>
      </ScrollArea.Root>
      <div className="mt-4 w-full">
        <Button className="w-full" onClick={addNotification}>
          <Plus className="h-4 w-4" />
          Trigger Notification
        </Button>
      </div>
    </div>
  )
}

export default ScrollAreaDemo

Installation

pnpm dlx shadcn@latest add https://registry.joyco.studio/r/scroll-area.json

With Chevron Demo

New comment

Sarah left a comment on your post

New follower

Alex started following you

Payment received

You received $250.00 from Client Co.

Reminder

Team standup meeting in 30 minutes

New message

Jordan sent you a direct message

Event tomorrow

Product launch scheduled for 9:00 AM

New comment

Sarah left a comment on your post

New follower

Alex started following you

'use client'

import { Button } from '@/components/ui/button'
import { cn } from '@/lib/utils'
import * as ScrollArea from '@/registry/joyco/blocks/scroll-area'
import {
  Bell,
  Calendar,
  ChevronDown,
  CreditCard,
  ChevronUp,
  Mail,
  Plus,
  MessageSquare,
  UserPlus,
  X,
} from 'lucide-react'
import { useEffect, useRef, useState } from 'react'

const notifications = [
  {
    icon: MessageSquare,
    title: 'New comment',
    description: 'Sarah left a comment on your post',
  },
  {
    icon: UserPlus,
    title: 'New follower',
    description: 'Alex started following you',
  },
  {
    icon: CreditCard,
    title: 'Payment received',
    description: 'You received $250.00 from Client Co.',
  },
  {
    icon: Bell,
    title: 'Reminder',
    description: 'Team standup meeting in 30 minutes',
  },
  {
    icon: Mail,
    title: 'New message',
    description: 'Jordan sent you a direct message',
  },
  {
    icon: Calendar,
    title: 'Event tomorrow',
    description: 'Product launch scheduled for 9:00 AM',
  },
]

function ChevronExample() {
  const [items, setItems] = useState<number[]>([0, 1, 2, 3, 4, 5, 6, 7])
  const nextIdRef = useRef(4)
  const scrollRef = useRef<HTMLDivElement>(null)

  useEffect(() => {
    if (scrollRef.current) {
      scrollRef.current.scrollTo({
        top: scrollRef.current.scrollHeight,
        behavior: 'smooth',
      })
    }
  }, [items.length])

  const addNotification = () => {
    setItems((prev) => [...prev, nextIdRef.current++])
  }

  const dismissNotification = (id: number) => {
    setItems((prev) => prev.filter((item) => item !== id))
  }

  return (
    <div className="mx-auto w-full max-w-md p-10">
      <ScrollArea.Root
        className="h-[400px] w-full"
        topShadowGradient="bg-linear-to-b from-card to-transparent"
        bottomShadowGradient="bg-linear-to-t from-card to-transparent"
      >
        {/* Scroll indicator arrows */}
        <div
          className={cn(
            'bg-background border-border pointer-events-none absolute top-2 left-1/2 z-30 -translate-x-1/2 rounded-full border p-0.5 transition-opacity duration-300',
            'group-data-[scroll-top=true]/scroll-area:opacity-100',
            'opacity-0'
          )}
        >
          <ChevronUp className="text-muted-foreground h-5 w-5" />
        </div>
        <div
          className={cn(
            'bg-background border-border pointer-events-none absolute bottom-2 left-1/2 z-30 -translate-x-1/2 rounded-full border p-0.5 transition-opacity duration-300',
            'group-data-[scroll-bottom=true]/scroll-area:opacity-100',
            'opacity-0'
          )}
        >
          <ChevronDown className="text-muted-foreground h-5 w-5" />
        </div>
        <ScrollArea.Content ref={scrollRef} className="space-y-2">
          {items.length === 0 ? (
            <div className="flex h-48 flex-col items-center justify-center text-center">
              <div className="bg-muted mb-3 rounded-full p-3">
                <Bell className="text-muted-foreground h-6 w-6" />
              </div>
              <p className="text-muted-foreground text-sm">No notifications</p>
            </div>
          ) : (
            items.map((id, index) => {
              const notification = notifications[id % notifications.length]
              const Icon = notification.icon
              return (
                <div
                  key={`${id}-${index}`}
                  className="bg-background rounded-lg border p-3"
                >
                  <div className="flex items-start gap-3">
                    <div className={cn('bg-muted rounded-sm p-2')}>
                      <Icon className={cn('text-muted-foreground h-5 w-5')} />
                    </div>
                    <div className="min-w-0 flex-1">
                      <h3 className="font-medium">{notification.title}</h3>
                      <p className="text-muted-foreground mt-0.5 text-xs">
                        {notification.description}
                      </p>
                    </div>
                    <button
                      onClick={() => dismissNotification(id)}
                      className="text-muted-foreground hover:text-foreground -mt-1 -mr-1 rounded p-1 transition-colors"
                    >
                      <X className="h-4 w-4" />
                    </button>
                  </div>
                </div>
              )
            })
          )}
        </ScrollArea.Content>
      </ScrollArea.Root>
      <div className="mt-4 w-full">
        <Button className="w-full" onClick={addNotification}>
          <Plus className="h-4 w-4" />
          Trigger Notification
        </Button>
      </div>
    </div>
  )
}

export default ChevronExample

Usage

import { ScrollArea } from '@/registry/joyco/blocks/scroll-area'
 
function App() {
  return (
    <ScrollArea.Root className="h-64">
      <ScrollArea.Content className="space-y-4 p-6">
        <div>Content here</div>
      </ScrollArea.Content>
    </ScrollArea.Root>
  )
}

On this page

Maintainers

Weekly Downloads

6Total
0 downloads today

Last updated on