Scroll Area
Custom thin scrollbars over native scrolling.
import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area";
const tokens = [ ["NVDAx", "32.0%"], ["MSFTx", "28.0%"], ["AAPLx", "24.0%"], ["ANTHRP-pre", "16.0%"], ["TSLAx", "—"], ["GOOGLx", "—"],];
export default function Demo() { return ( <div className="flex flex-wrap items-start gap-8"> <ScrollArea className="h-40 w-56 rounded-xl border border-border bg-card"> <div className="p-3"> <div className="mb-1 text-xs text-muted-foreground">Tokens</div> {tokens.map(([t, w]) => ( <div key={t} className="flex justify-between border-t border-border py-2 font-mono text-[13px]" > <span>{t}</span> <span className="text-muted-foreground">{w}</span> </div> ))} </div> </ScrollArea> <ScrollArea className="w-72 whitespace-nowrap"> <div className="flex gap-3 pb-4"> {["MAG4", "PREIPO", "AIDX", "ENRG"].map((n) => ( <div key={n} className="w-32 shrink-0 rounded-xl border border-border bg-card p-3" > <div className="font-mono text-[11px] text-muted-foreground"> {n} </div> <div className="mt-2 font-mono text-xl">$1.0412</div> </div> ))} </div> <ScrollBar orientation="horizontal" /> </ScrollArea> </div> );}<script setup lang="ts">import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area";
const tokens = [ ["NVDAx", "32.0%"], ["MSFTx", "28.0%"], ["AAPLx", "24.0%"], ["ANTHRP-pre", "16.0%"], ["TSLAx", "—"], ["GOOGLx", "—"],];const baskets = ["MAG4", "PREIPO", "AIDX", "ENRG"];</script>
<template> <div class="flex flex-wrap items-start gap-8"> <ScrollArea class="h-40 w-56 rounded-xl border border-border bg-card"> <div class="p-3"> <div class="mb-1 text-xs text-muted-foreground">Tokens</div> <div v-for="[t, w] in tokens" :key="t" class="flex justify-between border-t border-border py-2 font-mono text-[13px]" > <span>{{ t }}</span> <span class="text-muted-foreground">{{ w }}</span> </div> </div> </ScrollArea> <ScrollArea class="w-72 whitespace-nowrap"> <div class="flex gap-3 pb-4"> <div v-for="n in baskets" :key="n" class="w-32 shrink-0 rounded-xl border border-border bg-card p-3" > <div class="font-mono text-[11px] text-muted-foreground">{{ n }}</div> <div class="mt-2 font-mono text-xl">$1.0412</div> </div> </div> <ScrollBar orientation="horizontal" /> </ScrollArea> </div></template><script lang="ts"> import { ScrollArea } from "$lib/components/ui/scroll-area";
const tokens = [ ["NVDAx", "32.0%"], ["MSFTx", "28.0%"], ["AAPLx", "24.0%"], ["ANTHRP-pre", "16.0%"], ["TSLAx", "—"], ["GOOGLx", "—"], ]; const baskets = ["MAG4", "PREIPO", "AIDX", "ENRG"];</script>
<div class="flex flex-wrap items-start gap-8"> <ScrollArea class="h-40 w-56 rounded-xl border border-border bg-card"> <div class="p-3"> <div class="mb-1 text-xs text-muted-foreground">Tokens</div> {#each tokens as [t, w] (t)} <div class="flex justify-between border-t border-border py-2 font-mono text-[13px]"> <span>{t}</span> <span class="text-muted-foreground">{w}</span> </div> {/each} </div> </ScrollArea> <ScrollArea orientation="horizontal" class="w-72 whitespace-nowrap"> <div class="flex gap-3 pb-4"> {#each baskets as n (n)} <div class="w-32 shrink-0 rounded-xl border border-border bg-card p-3"> <div class="font-mono text-[11px] text-muted-foreground">{n}</div> <div class="mt-2 font-mono text-xl">$1.0412</div> </div> {/each} </div> </ScrollArea></div>Installation
Section titled “Installation”npx shadcn@latest add @edmi-ui/scroll-areapnpm dlx shadcn@latest add @edmi-ui/scroll-areayarn dlx shadcn@latest add @edmi-ui/scroll-areabunx shadcn@latest add @edmi-ui/scroll-areaPick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.
Install the dependencies:
Terminal window npm install @base-ui/reactTerminal window pnpm add @base-ui/reactTerminal window yarn add @base-ui/reactTerminal window bun add @base-ui/reactCopy the code from Component source into your project and update the import paths to match your aliases.
npx shadcn-vue@latest add @edmi-ui/scroll-areapnpm dlx shadcn-vue@latest add @edmi-ui/scroll-areayarn dlx shadcn-vue@latest add @edmi-ui/scroll-areabunx shadcn-vue@latest add @edmi-ui/scroll-areaPick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.
Install the dependencies:
Terminal window npm install reka-ui @vueuse/coreTerminal window pnpm add reka-ui @vueuse/coreTerminal window yarn add reka-ui @vueuse/coreTerminal window bun add reka-ui @vueuse/coreCopy the code from Component source into your project and update the import paths to match your aliases.
npx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/scroll-area.jsonpnpm dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/scroll-area.jsonyarn dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/scroll-area.jsonbunx --bun shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/scroll-area.jsonPick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.
Copy the code from Component source into your project and update the import paths to match your aliases.
import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area";
<div className="flex flex-wrap items-start gap-8"> <ScrollArea className="h-40 w-56 rounded-xl border border-border bg-card"> <div className="p-3"> <div className="mb-1 text-xs text-muted-foreground">Tokens</div> </div> </ScrollArea> <ScrollArea className="w-72 whitespace-nowrap"> <div className="flex gap-3 pb-4"> {["MAG4", "PREIPO", "AIDX", "ENRG"].map((n) => ( <div key={n} className="w-32 shrink-0 rounded-xl border border-border bg-card p-3" > <div className="font-mono text-[11px] text-muted-foreground"> {n} </div> <div className="mt-2 font-mono text-xl">$1.0412</div> </div> ))} </div> <ScrollBar orientation="horizontal" /> </ScrollArea></div><script setup lang="ts">import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area";</script>
<template> <div class="flex flex-wrap items-start gap-8"> <ScrollArea class="h-40 w-56 rounded-xl border border-border bg-card"> <div class="p-3"> <div class="mb-1 text-xs text-muted-foreground">Tokens</div> <div :key="t" class="flex justify-between border-t border-border py-2 font-mono text-[13px]" > <span>{{ t }}</span> <span class="text-muted-foreground">{{ w }}</span> </div> </div> </ScrollArea> <ScrollArea class="w-72 whitespace-nowrap"> <div class="flex gap-3 pb-4"> <div :key="n" class="w-32 shrink-0 rounded-xl border border-border bg-card p-3" > <div class="font-mono text-[11px] text-muted-foreground">{{ n }}</div> <div class="mt-2 font-mono text-xl">$1.0412</div> </div> </div> <ScrollBar orientation="horizontal" /> </ScrollArea> </div></template><script lang="ts"> import { ScrollArea } from "$lib/components/ui/scroll-area";</script>
<div class="flex flex-wrap items-start gap-8"> <ScrollArea class="h-40 w-56 rounded-xl border border-border bg-card"> <div class="p-3"> <div class="mb-1 text-xs text-muted-foreground">Tokens</div> {#each tokens as [t, w] (t)} <div class="flex justify-between border-t border-border py-2 font-mono text-[13px]"> <span>{t}</span> <span class="text-muted-foreground">{w}</span> </div> {/each} </div> </ScrollArea> <ScrollArea orientation="horizontal" class="w-72 whitespace-nowrap"> <div class="flex gap-3 pb-4"> {#each baskets as n (n)} <div class="w-32 shrink-0 rounded-xl border border-border bg-card p-3"> <div class="font-mono text-[11px] text-muted-foreground">{n}</div> <div class="mt-2 font-mono text-xl">$1.0412</div> </div> {/each} </div> </ScrollArea></div>Component source
Section titled “Component source”import { ScrollArea as ScrollAreaPrimitive } from "@base-ui/react/scroll-area";import { cn } from "cn";
function ScrollArea({ className, children, ...props}: ScrollAreaPrimitive.Root.Props) { return ( <ScrollAreaPrimitive.Root data-slot="scroll-area" className={cn("relative", className)} {...props} > <ScrollAreaPrimitive.Viewport data-slot="scroll-area-viewport" className="size-full rounded-[inherit] outline-none transition-[color,box-shadow] focus-visible:outline-2 focus-visible:outline-ring" > {children} </ScrollAreaPrimitive.Viewport> <ScrollBar /> <ScrollAreaPrimitive.Corner /> </ScrollAreaPrimitive.Root> );}
// Thin track-less bar: a 6px --input pill (kit.css .scrollbar).function ScrollBar({ className, orientation = "vertical", ...props}: ScrollAreaPrimitive.Scrollbar.Props) { return ( <ScrollAreaPrimitive.Scrollbar data-slot="scroll-area-scrollbar" data-orientation={orientation} orientation={orientation} className={cn( "flex touch-none select-none p-[3px] opacity-0 transition-opacity data-[hovering]:opacity-100 data-[scrolling]:opacity-100 data-[orientation=horizontal]:h-3 data-[orientation=horizontal]:flex-col data-[orientation=vertical]:h-full data-[orientation=vertical]:w-3", className, )} {...props} > <ScrollAreaPrimitive.Thumb data-slot="scroll-area-thumb" className="relative flex-1 rounded-full bg-input" /> </ScrollAreaPrimitive.Scrollbar> );}
export { ScrollArea, ScrollBar };<script setup lang="ts">import type { ScrollAreaRootProps } from 'reka-ui'import type { HTMLAttributes } from 'vue'import { reactiveOmit } from '@vueuse/core'import { ScrollAreaCorner, ScrollAreaRoot, ScrollAreaViewport } from 'reka-ui'import { cn } from '@/registry/edmi/lib/utils'import ScrollBar from './ScrollBar.vue'
const props = defineProps<ScrollAreaRootProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = reactiveOmit(props, 'class')</script>
<template> <ScrollAreaRoot data-slot="scroll-area" v-bind="delegatedProps" :class="cn('relative', props.class)" > <ScrollAreaViewport data-slot="scroll-area-viewport" class="size-full rounded-[inherit] outline-none transition-[color,box-shadow] focus-visible:outline-2 focus-visible:outline-ring" > <slot /> </ScrollAreaViewport> <ScrollBar /> <ScrollAreaCorner /> </ScrollAreaRoot></template><script setup lang="ts">import type { ScrollAreaScrollbarProps } from 'reka-ui'import type { HTMLAttributes } from 'vue'import { reactiveOmit } from '@vueuse/core'import { ScrollAreaScrollbar, ScrollAreaThumb } from 'reka-ui'import { cn } from '@/registry/edmi/lib/utils'
const props = withDefaults(defineProps<ScrollAreaScrollbarProps & { class?: HTMLAttributes['class'] }>(), { orientation: 'vertical',})
const delegatedProps = reactiveOmit(props, 'class')</script>
<template> <!-- Thin track-less bar: a 6px --input pill (kit.css .scrollbar). --> <ScrollAreaScrollbar data-slot="scroll-area-scrollbar" v-bind="delegatedProps" :class=" cn( 'flex touch-none select-none p-[3px] transition-opacity', orientation === 'vertical' && 'h-full w-3', orientation === 'horizontal' && 'h-3 flex-col', props.class, ) " > <ScrollAreaThumb data-slot="scroll-area-thumb" class="relative flex-1 rounded-full bg-input" /> </ScrollAreaScrollbar></template>export { default as ScrollArea } from "./ScrollArea.vue";export { default as ScrollBar } from "./ScrollBar.vue";<script lang="ts"> import { ScrollArea as ScrollAreaPrimitive } from "bits-ui"; import { cn, type WithoutChild } from "$lib/utils.js"; import { Scrollbar } from "./index.js";
let { ref = $bindable(null), viewportRef = $bindable(null), class: className, orientation = "vertical", scrollbarXClasses = "", scrollbarYClasses = "", children, ...restProps }: WithoutChild<ScrollAreaPrimitive.RootProps> & { orientation?: "vertical" | "horizontal" | "both" | undefined; scrollbarXClasses?: string | undefined; scrollbarYClasses?: string | undefined; viewportRef?: HTMLElement | null; } = $props();</script>
<ScrollAreaPrimitive.Root bind:ref data-slot="scroll-area" class={cn("relative", className)} {...restProps}> <ScrollAreaPrimitive.Viewport bind:ref={viewportRef} data-slot="scroll-area-viewport" class="size-full rounded-[inherit] outline-none transition-[color,box-shadow] focus-visible:outline-2 focus-visible:outline-ring" > {@render children?.()} </ScrollAreaPrimitive.Viewport> {#if orientation === "vertical" || orientation === "both"} <Scrollbar orientation="vertical" class={scrollbarYClasses} /> {/if} {#if orientation === "horizontal" || orientation === "both"} <Scrollbar orientation="horizontal" class={scrollbarXClasses} /> {/if} <ScrollAreaPrimitive.Corner /></ScrollAreaPrimitive.Root><script lang="ts"> import { ScrollArea as ScrollAreaPrimitive } from "bits-ui"; import { cn, type WithoutChild } from "$lib/utils.js";
let { ref = $bindable(null), class: className, orientation = "vertical", children, ...restProps }: WithoutChild<ScrollAreaPrimitive.ScrollbarProps> = $props();</script>
<!-- Thin track-less bar: a 6px --input pill (kit.css .scrollbar). --><ScrollAreaPrimitive.Scrollbar bind:ref data-slot="scroll-area-scrollbar" data-orientation={orientation} {orientation} class={cn( "flex touch-none select-none p-[3px] transition-opacity data-[orientation=horizontal]:h-3 data-[orientation=horizontal]:flex-col data-[orientation=vertical]:h-full data-[orientation=vertical]:w-3", className )} {...restProps}> {@render children?.()} <ScrollAreaPrimitive.Thumb data-slot="scroll-area-thumb" class="relative flex-1 rounded-full bg-input" /></ScrollAreaPrimitive.Scrollbar>import Root from "./scroll-area.svelte";import Scrollbar from "./scroll-area-scrollbar.svelte";
export { Root, //, Root as ScrollArea, Scrollbar, Scrollbar as ScrollAreaScrollbar,};| Prop | Type | Default |
|---|---|---|
ScrollBar orientation |
"vertical" | "horizontal" |
"vertical" |
Size the area with className (h-*, w-*).
✦ Edmi additions
Section titled “✦ Edmi additions”- Thumb is a 6px
--inputpill with no track.
Vue / Svelte differences
Section titled “Vue / Svelte differences”- Svelte:
ScrollAreatakesorientation="vertical" | "horizontal" | "both"; there is no separateScrollBarcomponent to add. - Vue: exports
ScrollBarlike React.