Skip to main content
astrocraft-ui/ components · 101

Scroll Area

Free · MIT

Headless, accessible, and styled entirely through the attribute surface below — bring your own CSS system, or start from the reference theme.

Install

Without a flag you get no styling at all — plain HTML carrying the attribute surface below. --theme copies the same theme in two dialects;--bridge lumos re-points either one onto Lumos for Astro's tokens (it needs a theme to re-point, and Lumos itself, already in your project).

Unstyled — behavior only, you write the CSS

Install command
npx astrocraft-ui add media/scroll-area

Plain-CSS theme — no build step

Install command
npx astrocraft-ui add media/scroll-area --theme css

Tailwind theme — needs Tailwind v4

Install command
npx astrocraft-ui add media/scroll-area --theme tailwind

Plain-CSS theme, re-pointed onto Lumos tokens

Install command
npx astrocraft-ui add media/scroll-area --theme css --bridge lumos

Tailwind theme, re-pointed onto Lumos tokens

Install command
npx astrocraft-ui add media/scroll-area --theme tailwind --bridge lumos

Live demo

ScrollArea

One attribute is the whole primitive. Tab into the log below and scroll it with the arrow keys — then note that it contains nothing focusable, which is the case browsers now handle themselves, and that the second one contains links, which is the case they still do not. Both are reachable here because tabindex="0" is in the markup rather than left to a heuristic.

  • 12:04:01 build started
  • 12:04:03 resolving 231 modules
  • 12:04:06 transformed src/components/ui/**
  • 12:04:09 bundling client scripts
  • 12:04:11 emitted dist/_astro/client.js
  • 12:04:12 emitted dist/index.html
  • 12:04:12 build finished in 11.4s
  • 12:04:12 0 errors, 0 warnings
  • 12:04:13 preview on http://localhost:4321

12:04:01 build started — details

12:04:03 resolving 231 modules — details

12:04:06 transformed src/components/ui/** — details

12:04:09 bundling client scripts — details

12:04:11 emitted dist/_astro/client.js — details

12:04:12 emitted dist/index.html — details

12:04:12 build finished in 11.4s — details

12:04:12 0 errors, 0 warnings — details

12:04:13 preview on http://localhost:4321 — details

Attribute surface

With no class-merging machinery, these attributes are the whole extension point. State is a data-attribute or a native attribute; visibility is the hiddenattribute. Generated from source by pnpm slots.

ComponentSlotsVariantsRuntime stateNative state
`ScrollArea.astro``scroll-area``data-orientation`: `vertical` · `horizontal` · `both`——

Source

What the command copies — 2 files, alias-free so the copy lands standing alone. This is the whole component; there is nothing else.

src/components/ui/media/scroll-area/ScrollArea.astro
---
// src/components/ui/media/scroll-area/ScrollArea.astro — headless primitive (see ../../README.md).
// A scroll container that a keyboard can actually reach. That is the entire primitive, and it is a
// real accessibility failure rather than a nicety: WCAG 2.1.1 requires content reachable by pointer
// to be reachable by keyboard, and an `overflow: auto` box is not — you can see the scrollbar, drag
// it with a mouse, and have no way to move it with a keyboard.
//
// Browsers have started closing that hole themselves, but only halfway: a scroller with NO focusable
// content becomes focusable automatically in recent Chrome, and a scroller WITH focusable content
// does not — on the theory that you can tab through the children instead. That theory fails the
// moment the scrollable content is a table, a log, a long code block, or a diagram, which is most of
// the times anyone reaches for this. So `tabindex="0"` is written here, unconditionally, in markup:
// it has to work with no JavaScript, and it has to work in every browser.
//
// A focusable box needs a name and a role, or a screen reader announces "group" and nothing else —
// hence `role="group"` and a `label`. `overflow` itself lives in structure.css, because without it
// this component is a plain <div> with a stray tab stop.
import "../../../../styles/structure.css";

// The rule guards against tab stops on inert content. A scroll container is the opposite of inert,
// and this tab stop is the entire primitive — see the note above on why the browser's own
// heuristic does not cover it.
/* eslint-disable astro/jsx-a11y/no-noninteractive-tabindex */
import type { HTMLAttributes } from "astro/types";

type Props = HTMLAttributes<"div"> & {
  /** Which axis may scroll. The other is clipped, so content cannot escape sideways. */
  orientation?: "vertical" | "horizontal" | "both";
  /** Names the region. A focusable box with no name is announced as nothing at all. */
  label?: string;
};

const {
  orientation = "vertical",
  label = "Scrollable content",
  class: className,
  ...rest
} = Astro.props;
---

<div
  tabindex="0"
  role="group"
  aria-label={label}
  data-slot="scroll-area"
  data-orientation={orientation}
  class={className}
  {...rest}
>
  <slot />
</div>
src/components/ui/media/scroll-area/index.ts
import ScrollArea from "./ScrollArea.astro";

export { ScrollArea };
export default ScrollArea;

What you get

The component source, copied into your project by npx astrocraft-ui add media/scroll-area — no package dependency, no CSS to fight. Whatever the paid blocks compose, this is it.