Skip to content
edmi-ui

Scroll Area

Custom thin scrollbars over native scrolling.

StackBlitz ↗StackBlitz ↗StackBlitz ↗
Tokens
NVDAx32.0%
MSFTx28.0%
AAPLx24.0%
ANTHRP-pre16.0%
TSLAx—
GOOGLx—
MAG4
$1.0412
PREIPO
$1.0412
AIDX
$1.0412
ENRG
$1.0412
Tokens
NVDAx 32.0%
MSFTx 28.0%
AAPLx 24.0%
ANTHRP-pre 16.0%
TSLAx —
GOOGLx —
MAG4
$1.0412
PREIPO
$1.0412
AIDX
$1.0412
ENRG
$1.0412
Terminal window
npx shadcn@latest add @edmi-ui/scroll-area
Terminal window
pnpm dlx shadcn@latest add @edmi-ui/scroll-area
Terminal window
yarn dlx shadcn@latest add @edmi-ui/scroll-area
Terminal window
bunx shadcn@latest add @edmi-ui/scroll-area

Pick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.

Terminal window
npx shadcn-vue@latest add @edmi-ui/scroll-area
Terminal window
pnpm dlx shadcn-vue@latest add @edmi-ui/scroll-area
Terminal window
yarn dlx shadcn-vue@latest add @edmi-ui/scroll-area
Terminal window
bunx shadcn-vue@latest add @edmi-ui/scroll-area

Pick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.

Terminal window
npx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/scroll-area.json
Terminal window
pnpm dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/scroll-area.json
Terminal window
yarn dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/scroll-area.json
Terminal window
bunx --bun shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/scroll-area.json

Pick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.

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>
registry/ui/scroll-area.tsx
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 };
registry/ui/scroll-area/ScrollArea.vue
<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>
registry/ui/scroll-area/ScrollBar.vue
<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>
registry/ui/scroll-area/index.ts
export { default as ScrollArea } from "./ScrollArea.vue";
export { default as ScrollBar } from "./ScrollBar.vue";
src/lib/registry/ui/scroll-area/scroll-area.svelte
<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>
src/lib/registry/ui/scroll-area/scroll-area-scrollbar.svelte
<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>
src/lib/registry/ui/scroll-area/index.ts
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-*).

  • Thumb is a 6px --input pill with no track.
  • Svelte: ScrollArea takes orientation="vertical" | "horizontal" | "both"; there is no separate ScrollBar component to add.
  • Vue: exports ScrollBar like React.