Scroll Area
Provides a consistent scroll area across platforms.
Scroll Area
Lorem ipsum dolor sit, amet consectetur adipisicing elit. Dignissimos impedit rem, repellat deserunt ducimus quasi nisi voluptatem cumque aliquid esse ea deleniti eveniet incidunt! Deserunt minus laborum accusamus iusto dolorum. Lorem ipsum dolor sit, amet consectetur adipisicing elit. Blanditiis officiis error minima eos fugit voluptate excepturi eveniet dolore et, ratione impedit consequuntur dolorem hic quae corrupti autem? Dolorem, sit voluptatum.
<script lang="ts">
import { ScrollArea } from "bits-ui";
</script>
<ScrollArea.Root
class="relative overflow-hidden rounded-[10px] border border-dark-10 bg-background-alt px-4 py-4 shadow-card"
>
<ScrollArea.Viewport class="h-full max-h-[200px] w-full max-w-[200px]">
<h4
class="mb-4 mt-2 text-xl font-semibold leading-none tracking-[-0.01em] text-foreground"
>
Scroll Area
</h4>
<p class="text-wrap text-sm leading-5 text-foreground-alt">
Lorem ipsum dolor sit, amet consectetur adipisicing elit. Dignissimos
impedit rem, repellat deserunt ducimus quasi nisi voluptatem cumque
aliquid esse ea deleniti eveniet incidunt! Deserunt minus laborum
accusamus iusto dolorum. Lorem ipsum dolor sit, amet consectetur
adipisicing elit. Blanditiis officiis error minima eos fugit voluptate
excepturi eveniet dolore et, ratione impedit consequuntur dolorem hic quae
corrupti autem? Dolorem, sit voluptatum.
</p>
</ScrollArea.Viewport>
<ScrollArea.Scrollbar
orientation="vertical"
class="flex w-2.5 touch-none select-none rounded-full border-l border-l-transparent bg-muted p-px transition-all duration-200 hover:w-3 hover:bg-dark-10 data-[state=visible]:animate-in data-[state=hidden]:animate-out data-[state=hidden]:fade-out-0 data-[state=visible]:fade-in-0"
>
<ScrollArea.Thumb class="flex-1 rounded-full bg-muted-foreground" />
</ScrollArea.Scrollbar>
<ScrollArea.Scrollbar
orientation="horizontal"
class="flex h-2.5 touch-none select-none rounded-full border-t border-t-transparent bg-muted p-px transition-all duration-200 hover:h-3 hover:bg-dark-10 "
>
<ScrollArea.Thumb class="rounded-full bg-muted-foreground" />
</ScrollArea.Scrollbar>
<ScrollArea.Corner />
</ScrollArea.Root>
import typography from "@tailwindcss/typography";
import animate from "tailwindcss-animate";
import { fontFamily } from "tailwindcss/defaultTheme";
/** @type {import('tailwindcss').Config} */
export default {
darkMode: "class",
content: ["./src/**/*.{html,js,svelte,ts}"],
theme: {
container: {
center: true,
screens: {
"2xl": "1440px",
},
},
extend: {
colors: {
border: {
DEFAULT: "hsl(var(--border-card))",
input: "hsl(var(--border-input))",
"input-hover": "hsl(var(--border-input-hover))",
},
background: {
DEFAULT: "hsl(var(--background) / <alpha-value>)",
alt: "hsl(var(--background-alt) / <alpha-value>)",
},
foreground: {
DEFAULT: "hsl(var(--foreground) / <alpha-value>)",
alt: "hsl(var(--foreground-alt) / <alpha-value>)",
},
muted: {
DEFAULT: "hsl(var(--muted) / <alpha-value>)",
foreground: "hsl(var(--muted-foreground))",
},
dark: {
DEFAULT: "hsl(var(--dark) / <alpha-value>)",
4: "hsl(var(--dark-04))",
10: "hsl(var(--dark-10))",
40: "hsl(var(--dark-40))",
},
accent: {
DEFAULT: "hsl(var(--accent) / <alpha-value>)",
foreground: "hsl(var(--accent-foreground) / <alpha-value>)",
},
destructive: {
DEFAULT: "hsl(var(--destructive) / <alpha-value>)",
},
contrast: {
DEFAULT: "hsl(var(--contrast) / <alpha-value>)",
},
},
fontFamily: {
sans: ["Inter", ...fontFamily.sans],
mono: ["Source Code Pro", ...fontFamily.mono],
alt: ["Courier", ...fontFamily.sans],
},
fontSize: {
xxs: "10px",
},
borderWidth: {
6: "6px",
},
borderRadius: {
card: "16px",
"card-lg": "20px",
"card-sm": "10px",
input: "9px",
button: "5px",
"5px": "5px",
"9px": "9px",
"10px": "10px",
"15px": "15px",
},
height: {
input: "3rem",
"input-sm": "2.5rem",
},
boxShadow: {
mini: "var(--shadow-mini)",
"mini-inset": "var(--shadow-mini-inset)",
popover: "var(--shadow-popover)",
kbd: "var(--shadow-kbd)",
btn: "var(--shadow-btn)",
card: "var(--shadow-card)",
"date-field-focus": "var(--shadow-date-field-focus)",
},
opacity: {
8: "0.08",
},
scale: {
80: ".80",
98: ".98",
99: ".99",
},
},
keyframes: {
"accordion-down": {
from: { height: "0" },
to: { height: "var(--bits-accordion-content-height)" },
},
"accordion-up": {
from: { height: "var(--bits-accordion-content-height)" },
to: { height: "0" },
},
"caret-blink": {
"0%,70%,100%": { opacity: "1" },
"20%,50%": { opacity: "0" },
},
},
animation: {
"accordion-down": "accordion-down 0.2s ease-out",
"accordion-up": "accordion-up 0.2s ease-out",
"caret-blink": "caret-blink 1.25s ease-out infinite",
},
},
plugins: [typography, animate],
};
@import url("https://fonts.googleapis.com/css2?family=Source+Code+Pro:ital,wght@0,400;0,500;0,600;0,700;1,400;1,500;1,600;1,700&display=swap");
@import url("https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap");
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
/* Colors */
--background: 0 0% 100%;
--background-alt: 0 0% 100%;
--foreground: 0 0% 9%;
--foreground-alt: 0 0% 32%;
--muted: 240 5% 96%;
--muted-foreground: 0 0% 9% / 0.4;
--border: 240 6% 10%;
--border-input: 240 6% 10% / 0.17;
--border-input-hover: 240 6% 10% / 0.4;
--border-card: 240 6% 10% / 0.1;
--dark: 240 6% 10%;
--dark-10: 240 6% 10% / 0.1;
--dark-40: 240 6% 10% / 0.4;
--dark-04: 240 6% 10% / 0.04;
--accent: 204 94% 94%;
--accent-foreground: 204 80% 16%;
--destructive: 347 77% 50%;
/* black */
--constrast: 0 0% 0%;
/* Shadows */
--shadow-mini: 0px 1px 0px 1px rgba(0, 0, 0, 0.04);
--shadow-mini-inset: 0px 1px 0px 0px rgba(0, 0, 0, 0.04) inset;
--shadow-popover: 0px 7px 12px 3px hsla(var(--dark-10));
--shadow-kbd: 0px 2px 0px 0px rgba(0, 0, 0, 0.07);
--shadow-btn: 0px 1px 0px 1px rgba(0, 0, 0, 0.03);
--shadow-card: 0px 2px 0px 1px rgba(0, 0, 0, 0.04);
--shadow-date-field-focus: 0px 0px 0px 3px rgba(24, 24, 27, 0.17);
}
.dark {
/* Colors */
--background: 0 0% 5%;
--background-alt: 0 0% 8%;
--foreground: 0 0% 95%;
--foreground-alt: 0 0% 70%;
--muted: 240 4% 16%;
--muted-foreground: 0 0% 100% / 0.4;
--border: 0 0% 96%;
--border-input: 0 0% 96% / 0.17;
--border-input-hover: 0 0% 96% / 0.4;
--border-card: 0 0% 96% / 0.1;
--dark: 0 0% 96%;
--dark-40: 0 0% 96% / 0.4;
--dark-10: 0 0% 96% / 0.1;
--dark-04: 0 0% 96% / 0.04;
--accent: 204 90 90%;
--accent-foreground: 204 94% 94%;
--destructive: 350 89% 60%;
/* white */
--constrast: 0 0% 100%;
/* Shadows */
--shadow-mini: 0px 1px 0px 1px rgba(0, 0, 0, 0.3);
--shadow-mini-inset: 0px 1px 0px 0px rgba(0, 0, 0, 0.5) inset;
--shadow-popover: 0px 7px 12px 3px hsla(0deg 0% 0% / 30%);
--shadow-kbd: 0px 2px 0px 0px rgba(255, 255, 255, 0.07);
--shadow-btn: 0px 1px 0px 1px rgba(0, 0, 0, 0.2);
--shadow-card: 0px 2px 0px 1px rgba(0, 0, 0, 0.4);
--shadow-date-field-focus: 0px 0px 0px 3px rgba(244, 244, 245, 0.1);
}
}
@layer base {
* {
@apply border-border;
}
html {
-webkit-text-size-adjust: 100%;
font-variation-settings: normal;
}
body {
@apply bg-background text-foreground;
font-feature-settings:
"rlig" 1,
"calt" 1;
}
/* Mobile tap highlight */
/* https://developer.mozilla.org/en-US/docs/Web/CSS/-webkit-tap-highlight-color */
html {
-webkit-tap-highlight-color: rgba(128, 128, 128, 0.5);
}
::selection {
background: #fdffa4;
color: black;
}
/* === Scrollbars === */
::-webkit-scrollbar {
@apply w-2;
@apply h-2;
}
::-webkit-scrollbar-track {
@apply !bg-transparent;
}
::-webkit-scrollbar-thumb {
@apply rounded-card-lg !bg-dark-10;
}
::-webkit-scrollbar-corner {
background: rgba(0, 0, 0, 0);
}
/* Firefox */
/* https://developer.mozilla.org/en-US/docs/Web/CSS/scrollbar-color#browser_compatibility */
html {
scrollbar-color: var(--bg-muted);
}
.antialised {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
}
@layer utilities {
.step {
counter-increment: step;
}
.step:before {
@apply absolute inline-flex h-9 w-9 items-center justify-center rounded-full border-4 border-background bg-muted text-center -indent-px font-mono text-base font-medium;
@apply ml-[-50px] mt-[-4px];
content: counter(step);
}
}
@layer components {
*:not(body):not(.focus-override) {
outline: none !important;
&:focus-visible {
@apply focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-foreground focus-visible:ring-offset-2 focus-visible:ring-offset-background;
}
}
.link {
@apply inline-flex items-center gap-1 rounded-sm font-medium underline underline-offset-4 hover:text-foreground/80 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-foreground focus-visible:ring-offset-2 focus-visible:ring-offset-background;
}
input::-webkit-outer-spin-button,
input::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
/* Firefox */
input[type="number"] {
-moz-appearance: textfield;
}
}
Structure
<script lang="ts">
import { ScrollArea } from "bits-ui";
</script>
<ScrollArea.Root>
<ScrollArea.Viewport>
<!-- Scrollable content here -->
</ScrollArea.Viewport>
<ScrollArea.Scrollbar orientation="vertical">
<ScrollArea.Thumb />
</ScrollArea.Scrollbar>
<ScrollArea.Scrollbar orientation="horizontal">
<ScrollArea.Thumb />
</ScrollArea.Scrollbar>
<ScrollArea.Corner />
</ScrollArea.Root>
Reusable Components
If you're planning to use the Scroll Area throughout your application, it's recommended to create a reusable component to reduce the amount of code you need to write each time.
This example shows you how to create a Scroll Area component that accepts a few custom props that make it more capable.
<script lang="ts">
import { ScrollArea, type WithoutChild } from "bits-ui";
type Props = WithoutChild<ScrollArea.RootProps> & {
orientation: "vertical" | "horizontal" | "both";
viewportClasses?: string;
};
let {
ref = $bindable(null),
orientation = "vertical",
viewportClasses,
children,
...restProps
}: Props = $props();
</script>
{#snippet Scrollbar({ orientation }: { orientation: "vertical" | "horizontal" })}
<ScrollArea.Scrollbar {orientation}>
<ScrollArea.Thumb />
</ScrollArea.Scrollbar>
{/snippet}
<ScrollArea.Root bind:ref {...restProps}>
<ScrollArea.Viewport class={viewportClasses}>
{@render children?.()}
</ScrollArea.Viewport>
{#if orientation === "vertical" || orientation === "both"}
{@render Scrollbar({ orientation: "vertical" })}
{/if}
{#if orientation === "horizontal" || orientation === "both"}
{@render Scrollbar({ orientation: "horizontal" })}
{/if}
<ScrollArea.Corner />
</ScrollArea.Root>
We'll use this custom component in the following examples to demonstrate how to customize the behavior of the Scroll Area.
Scroll Area Types
Hover
The hover
type is the default type of the scroll area, demonstrated in the featured example above. It only shows scrollbars when the user hovers over the scroll area and the content is larger than the viewport.
<MyScrollArea type="hover">
<!-- ... -->
</MyScrollArea>
Scroll Area
Lorem ipsum dolor sit, amet consectetur adipisicing elit. Dignissimos impedit rem, repellat deserunt ducimus quasi nisi voluptatem cumque aliquid esse ea deleniti eveniet incidunt! Deserunt minus laborum accusamus iusto dolorum. Lorem ipsum dolor sit, amet consectetur adipisicing elit. Blanditiis officiis error minima eos fugit voluptate excepturi eveniet dolore et, ratione impedit consequuntur dolorem hic quae corrupti autem? Dolorem, sit voluptatum.
Scroll
The scroll
type displays the scrollbars when the user scrolls the content. This is similar to the behavior of MacOS.
<MyScrollArea type="scroll">
<!-- ... -->
</MyScrollArea>
Scroll Area
Lorem ipsum dolor sit, amet consectetur adipisicing elit. Dignissimos impedit rem, repellat deserunt ducimus quasi nisi voluptatem cumque aliquid esse ea deleniti eveniet incidunt! Deserunt minus laborum accusamus iusto dolorum. Lorem ipsum dolor sit, amet consectetur adipisicing elit. Blanditiis officiis error minima eos fugit voluptate excepturi eveniet dolore et, ratione impedit consequuntur dolorem hic quae corrupti autem? Dolorem, sit voluptatum.
Auto
The auto
type behaves similarly to your typical browser scrollbars. When the content is larger than the viewport, the scrollbars will appear and remain visible at all times.
<MyScrollArea type="auto">
<!-- ... -->
</MyScrollArea>
Scroll Area
Lorem ipsum dolor sit, amet consectetur adipisicing elit. Dignissimos impedit rem, repellat deserunt ducimus quasi nisi voluptatem cumque aliquid esse ea deleniti eveniet incidunt! Deserunt minus laborum accusamus iusto dolorum. Lorem ipsum dolor sit, amet consectetur adipisicing elit. Blanditiis officiis error minima eos fugit voluptate excepturi eveniet dolore et, ratione impedit consequuntur dolorem hic quae corrupti autem? Dolorem, sit voluptatum.
Always
The always
type behaves as if you set overflow: scroll
on the scroll area. Scrollbars will always be visible, even when the content is smaller than the viewport. We've also set the orientation
prop on the MyScrollArea
to 'both'
to ensure both scrollbars are rendered.
<MyScrollArea type="always" orientation="both">
<!-- ... -->
</MyScrollArea>
Scroll Area
Lorem ipsum dolor sit, amet consectetur adipisicing elit. Dignissimos impedit rem, repellat deserunt ducimus quasi nisi voluptatem cumque aliquid esse ea deleniti eveniet incidunt! Deserunt minus laborum accusamus iusto dolorum. Lorem ipsum dolor sit, amet consectetur adipisicing elit. Blanditiis officiis error minima eos fugit voluptate excepturi eveniet dolore et, ratione impedit consequuntur dolorem hic quae corrupti autem? Dolorem, sit voluptatum.
Customizing the Hide Delay
You can customize the hide delay of the scrollbars using the scrollHideDelay
prop.
<MyScrollArea scrollHideDelay={10}>
<!-- ... -->
</MyScrollArea>
Scroll Area
Lorem ipsum dolor sit, amet consectetur adipisicing elit. Dignissimos impedit rem, repellat deserunt ducimus quasi nisi voluptatem cumque aliquid esse ea deleniti eveniet incidunt! Deserunt minus laborum accusamus iusto dolorum. Lorem ipsum dolor sit, amet consectetur adipisicing elit. Blanditiis officiis error minima eos fugit voluptate excepturi eveniet dolore et, ratione impedit consequuntur dolorem hic quae corrupti autem? Dolorem, sit voluptatum.
API Reference
The container of all scroll area components. Overflow is hidden on this element to prevent double scrollbars.
Property | Type | Description |
---|---|---|
type | enum | The type of scroll area. Default: 'hover' |
scrollHideDelay | number | The delay in milliseconds before the scroll area hides itself when using Default: 600 |
dir | enum | The reading direction of the app. Default: 'ltr' |
ref $bindable | HTMLDivElement | The underlying DOM element being rendered. You can bind to this to get a reference to the element. Default: undefined |
children | Snippet | The children content to render. Default: undefined |
child | Snippet | Use render delegation to render your own element. See Child Snippet docs for more information. Default: undefined |
Data Attribute | Value | Description |
---|---|---|
data-scroll-area-root | '' | Present on the root element. |
The component which wraps the content and is responsible for computing the scroll area size.
Property | Type | Description |
---|---|---|
ref $bindable | HTMLDivElement | The underlying DOM element being rendered. You can bind to this to get a reference to the element. Default: undefined |
children | Snippet | The children content to render. Default: undefined |
Data Attribute | Value | Description |
---|---|---|
data-scroll-area-viewport | '' | Present on the viewport element. |
A scrollbar of the scroll area.
Property | Type | Description |
---|---|---|
orientation required | enum | The orientation of the scrollbar. Default: undefined |
forceMount | boolean | Whether or not to forcefully mount the content. This is useful if you want to use Svelte transitions or another animation library for the content. Default: false |
ref $bindable | HTMLDivElement | The underlying DOM element being rendered. You can bind to this to get a reference to the element. Default: undefined |
children | Snippet | The children content to render. Default: undefined |
child | Snippet | Use render delegation to render your own element. See Child Snippet docs for more information. Default: undefined |
Data Attribute | Value | Description |
---|---|---|
data-state | enum | The visibility state of the scrollbar |
data-scroll-area-scrollbar-x | '' | Present on the |
data-scroll-area-scrollbar-y | '' | Present on the |
A thumb of a scrollbar in the scroll area.
Property | Type | Description |
---|---|---|
forceMount | boolean | Whether or not to forcefully mount the content. This is useful if you want to use Svelte transitions or another animation library for the content. Default: false |
ref $bindable | HTMLDivElement | The underlying DOM element being rendered. You can bind to this to get a reference to the element. Default: undefined |
children | Snippet | The children content to render. Default: undefined |
child | Snippet | Use render delegation to render your own element. See Child Snippet docs for more information. Default: undefined |
Data Attribute | Value | Description |
---|---|---|
data-state | enum | The visibility state of the scrollbar |
data-scroll-area-thumb-x | '' | Present on the |
data-scroll-area-thumb-y | '' | Present on the |
The corner element between the X and Y scrollbars.
Property | Type | Description |
---|---|---|
ref $bindable | HTMLDivElement | The underlying DOM element being rendered. You can bind to this to get a reference to the element. Default: undefined |
children | Snippet | The children content to render. Default: undefined |
child | Snippet | Use render delegation to render your own element. See Child Snippet docs for more information. Default: undefined |
Data Attribute | Value | Description |
---|---|---|
data-scroll-area-corner | '' | Present on the corner element. |