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.
Core Principles
Section titled “Core Principles”- 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.
- Accessibility built-in. Components follow WAI-ARIA design patterns. Interactive components use
@data-slotlibraries that handle focus management, keyboard navigation, and screen reader support out of the box. - Folder-per-component structure. Each component family lives in its own folder (e.g.,
card/,dialog/). Sub-components are separate.astrofiles, and the folder exports them via a barrelindex.tsfile. - 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. - Composable. We prefer small primitives over heavy abstractions. Code should be easy to read, copy, and modify.
- Astro-native, zero-JS by default. Components are server-first and ship no client JavaScript unless they truly need it.
- 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.
- 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.
Composition
Section titled “Composition”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:
Cardis the root card container.CardHeaderrenders the header section and containsCardTitleandCardDescription.CardContentandCardFooterare sibling sections within the card.
Each subcomponent is its own .astro file within the card/ folder, and all are exported from card/index.ts.
How the folder structure works
Section titled “How the folder structure works”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.
Why use separate files?
Section titled “Why use separate files?”We split components into separate files because:
- Each file is focused.
CardTitle.astroonly handles the title - its props, styles, and behavior are all in one place. - Import only what you need. If you only use
CardandCardContent, you don’t need to import the others. - Easier to extend. Adding a new subcomponent means creating a new file and adding it to the barrel export.
- Better IDE support. Separate files give better autocomplete and jump-to-definition behavior.
Consistent patterns across components
Section titled “Consistent patterns across components”All multi-part components follow the same structure:
---// Dialogimport { 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>---// Selectimport { 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.
JavaScript Interactions
Section titled “JavaScript Interactions”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
How it works
Section titled “How it works”Components that need JavaScript use data-slot attributes to mark interactive elements. The corresponding @data-slot library initializes these elements with the required behavior:
<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.
Why @data-slot?
Section titled “Why @data-slot?”- Separation of concerns. UI components define structure and styles. Interactive behavior is maintained separately and can be tested independently.
- Framework-agnostic. Unlike Radix (React-only) or Headless UI (React/Vue),
@data-slotdoes not require a framework to be installed. - Progressive enhancement. Components render server-side with full HTML and CSS. JavaScript adds interactive behavior after hydration.
- Minimal bundle size. Each library is small and focused. You only ship the JavaScript needed for components you actually use.
- Accessible by default. All
@data-slotlibraries follow WAI-ARIA design patterns and handle focus management, keyboard navigation, and screen reader support.
Which components use @data-slot?
Section titled “Which components use @data-slot?”Currently, three components use @data-slot libraries:
- Dialog — Uses
@data-slot/dialogfor modal behavior - Accordion — Uses
@data-slot/accordionfor collapsible sections - Tabs — Uses
@data-slot/tabsfor tabbed interfaces
All other components are CSS-only and ship zero JavaScript to the client.