Skip to content
Theme Editor
⌘E
Background
L 0.5
C 0.2
H 250
Foreground
L 0.5
C 0.2
H 250
Primary
L 0.5
C 0.2
H 250
Primary FG
L 0.5
C 0.2
H 250
Secondary
L 0.5
C 0.2
H 250
Secondary FG
L 0.5
C 0.2
H 250
Accent
L 0.5
C 0.2
H 250
Accent FG
L 0.5
C 0.2
H 250
Card
L 0.5
C 0.2
H 250
Card FG
L 0.5
C 0.2
H 250
Popover
L 0.5
C 0.2
H 250
Popover FG
L 0.5
C 0.2
H 250
Muted
L 0.5
C 0.2
H 250
Muted FG
L 0.5
C 0.2
H 250
Border
L 0.5
C 0.2
H 250
Input
L 0.5
C 0.2
H 250
Ring
L 0.5
C 0.2
H 250
Destructive
L 0.5
C 0.2
H 250
Destructive FG
L 0.5
C 0.2
H 250
Chart 1
L 0.5
C 0.2
H 250
Chart 2
L 0.5
C 0.2
H 250
Chart 3
L 0.5
C 0.2
H 250
Chart 4
L 0.5
C 0.2
H 250
Chart 5
L 0.5
C 0.2
H 250

Design Principles

A system for creating UI libraries and design systems with built-in governance, accessibility, and documentation.

bejamas/ui is a system for creating UI libraries and design systems in Astro. It handles governance around UI components, accessibility, and component documentation — providing structure and patterns that scale from small component libraries to full design systems.

The way components are structured is as important as how they look.

  1. Docs in code. Documentation is auto-generated from structured comments within each component file (inspired by JSDoc). There’s no separate source of truth to maintain — the code IS the documentation.
  2. Accessibility built-in. Components follow WAI-ARIA design patterns. Interactive components use @data-slot libraries that handle focus management, keyboard navigation, and screen reader support out of the box.
  3. Folder-per-component structure. Each component family lives in its own folder (e.g., card/, dialog/). Sub-components are separate .astro files, and the folder exports them via a barrel index.ts file.
  4. Themeable. Components use token-based classes (e.g. bg-background, text-primary) instead of hard-coded colors, making it easy to adapt to any brand.
  5. Composable. We prefer small primitives over heavy abstractions. Code should be easy to read, copy, and modify.
  6. Astro-native, zero-JS by default. Components are server-first and ship no client JavaScript unless they truly need it.
  7. Framework-agnostic. We don’t bundle React, Vue, or any other client framework. You’re free to add your own Astro islands or framework of choice on top where needed.
  8. shadcn/ui aligned. We follow shadcn’s markup patterns and class naming where useful, so you can reuse component examples and patterns from the shadcn registry and adapt them easily for Astro.

React libraries often use subcomponents:

import {
Card,
CardHeader,
CardTitle,
CardDescription,
} from "@bejamas/ui/components/card";
<Card>
<CardHeader>
<CardTitle>Title</CardTitle>
<CardDescription>Description</CardDescription>
</CardHeader>
</Card>;

In Astro, we use the same pattern with barrel exports:

---
import {
Card,
CardHeader,
CardTitle,
CardDescription,
CardContent,
CardFooter,
} from "@bejamas/ui/components/card";
---
<Card>
<CardHeader>
<CardTitle>Title</CardTitle>
<CardDescription>Description</CardDescription>
</CardHeader>
<CardContent> Card content </CardContent>
<CardFooter> Footer actions </CardFooter>
</Card>

Here:

  • Card is the root card container.
  • CardHeader renders the header section and contains CardTitle and CardDescription.
  • CardContent and CardFooter are sibling sections within the card.

Each subcomponent is its own .astro file within the card/ folder, and all are exported from card/index.ts.

Each component family has its own folder:

components/
├── card/
│ ├── Card.astro
│ ├── CardHeader.astro
│ ├── CardTitle.astro
│ ├── CardDescription.astro
│ ├── CardContent.astro
│ ├── CardFooter.astro
│ └── index.ts
├── button/
│ ├── Button.astro
│ └── index.ts
└── ...

The index.ts barrel file exports all subcomponents:

export { default as Card } from "./Card.astro";
export { default as CardHeader } from "./CardHeader.astro";
export { default as CardTitle } from "./CardTitle.astro";
export { default as CardDescription } from "./CardDescription.astro";
export { default as CardContent } from "./CardContent.astro";
export { default as CardFooter } from "./CardFooter.astro";

This gives us:

  • clear, predictable import paths,
  • each subcomponent is focused and easy to understand,
  • users can import just what they need,
  • and consistent patterns across all component families.

We split components into separate files because:

  1. Each file is focused. CardTitle.astro only handles the title - its props, styles, and behavior are all in one place.
  2. Import only what you need. If you only use Card and CardContent, you don’t need to import the others.
  3. Easier to extend. Adding a new subcomponent means creating a new file and adding it to the barrel export.
  4. Better IDE support. Separate files give better autocomplete and jump-to-definition behavior.

All multi-part components follow the same structure:

---
// Dialog
import {
Dialog,
DialogTrigger,
DialogClose,
DialogHeader,
DialogTitle,
DialogDescription,
DialogContent,
DialogFooter,
} from "@bejamas/ui/components/dialog";
---
<Dialog>
<DialogTrigger>Open Dialog</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Dialog Title</DialogTitle>
<DialogDescription>Dialog description text.</DialogDescription>
</DialogHeader>
<p>Dialog body content goes here.</p>
<DialogFooter>
<DialogClose>Close</DialogClose>
</DialogFooter>
</DialogContent>
</Dialog>
---
// Select
import {
Select,
SelectControl,
SelectOption,
SelectIndicator,
} from "@bejamas/ui/components/select";
---
<Select>
<SelectIndicator />
<SelectControl>
<SelectOption value="apple">Apple</SelectOption>
<SelectOption value="banana">Banana</SelectOption>
</SelectControl>
</Select>

This consistency makes the library predictable - once you learn one component, you know how to use them all.


While most components are purely CSS-based, some require JavaScript for interactive behavior like dialogs, accordions, and tabs. Rather than bundling framework-specific code or writing custom scripts for each component, we use @data-slot — a set of headless UI libraries designed and maintained by Bejamas as a separate project.

@data-slot is a collection of framework-agnostic, headless UI libraries that provide accessible interactive behavior through simple JavaScript functions. Each library handles a specific UI pattern:

  • @data-slot/dialog — Modal dialogs with focus trapping, escape-to-close, and backdrop clicks
  • @data-slot/accordion — Collapsible sections with keyboard navigation
  • @data-slot/tabs — Tabbed interfaces with ARIA support

Components that need JavaScript use data-slot attributes to mark interactive elements. The corresponding @data-slot library initializes these elements with the required behavior:

Dialog.astro
<div data-slot="dialog">
<slot />
</div>
<script>
import { createDialog } from "@data-slot/dialog";
document
.querySelectorAll('[data-slot="dialog"]')
.forEach((el) => createDialog(el));
</script>

The Astro component remains purely presentational — it renders HTML with data attributes. The <script> tag runs client-side to enhance those elements with JavaScript behavior.

  1. Separation of concerns. UI components define structure and styles. Interactive behavior is maintained separately and can be tested independently.
  2. Framework-agnostic. Unlike Radix (React-only) or Headless UI (React/Vue), @data-slot does not require a framework to be installed.
  3. Progressive enhancement. Components render server-side with full HTML and CSS. JavaScript adds interactive behavior after hydration.
  4. Minimal bundle size. Each library is small and focused. You only ship the JavaScript needed for components you actually use.
  5. Accessible by default. All @data-slot libraries follow WAI-ARIA design patterns and handle focus management, keyboard navigation, and screen reader support.

Currently, three components use @data-slot libraries:

  • Dialog — Uses @data-slot/dialog for modal behavior
  • Accordion — Uses @data-slot/accordion for collapsible sections
  • Tabs — Uses @data-slot/tabs for tabbed interfaces

All other components are CSS-only and ship zero JavaScript to the client.