Skip to content

Understanding AgnosticUI ​

Experimental Alpha

This library is a work-in-progress. We are releasing it early to gather feedback, but it is not ready for production.

This guide explains the core concepts, architecture, and philosophy behind AgnosticUI v2.

The Local-First Philosophy ​

AgnosticUI v2 introduces a paradigm shift in how you consume UI components: instead of relying on packages in node_modules, components live directly in your codebase.

Why Local-First? ​

Traditional Approach Problems:

  • Components are black boxes you can't easily modify
  • Customization requires complex theming APIs or wrapping
  • AI tools can't see component internals
  • Breaking changes on updates can break your app

Local-First Benefits:

  • Full Ownership - Components are yours to modify freely
  • AI-Friendly - Full context visible to Cursor, Claude, Windsurf, etc.
  • Zero Lock-In - No vendor dependency, copy and customize
  • Safe Upgrades - Reference library + your copies = safe exploration

Two-Tier Architecture ​

AgnosticUI uses a unique two-tier system:

your-project/
├── agnosticui/              # Reference library (read-only context)
│   ├── lib/                 # Full component source
│   ├── tokens/              # Design tokens
│   └── components.json      # Component registry
│
├── src/components/ag/       # Your components (fully yours)
│   ├── Button/
│   ├── Input/
│   └── styles/
└── agnosticui.config.json   # CLI configuration

Reference Library (./agnosticui/)

  • Complete source code for all components
  • Provides context for AI tools
  • Updated independently via ag sync
  • Never modified directly

Your Components (./src/components/ag/)

  • Copies of components you've added
  • Fully customizable and owned by you
  • Safe to modify without breaking reference
  • Can be re-synced with --force if needed

Architecture Overview ​

Built on Web Components ​

AgnosticUI uses Lit as its foundation. Lit is a lightweight (5KB) library for building web components with a declarative template syntax and reactive properties.

Why Web Components?

  • Universal - Works with any framework
  • Standards-based - Built on web platform APIs
  • Encapsulated - Shadow DOM prevents style conflicts
  • Future-proof - Browser-native technology

Framework Integration Layers ​

AgnosticUI provides three integration approaches:

1. Core Lit Components (Universal) ​

The foundation layer that works everywhere:

typescript
import "./components/ag/Button/core/Button.js";

// Use as custom element
<ag-button variant="primary">Click me</ag-button>

Best for: Lit apps, Svelte, Angular, Solid, and other frameworks

2. React Wrappers (React-specific) ​

React-friendly wrappers using @lit/react:

typescript
import { ReactButton } from "./components/ag/Button/react/ReactButton";

function App() {
  return <ReactButton variant="primary">Click me</ReactButton>;
}

Benefits:

  • Natural React props API
  • Automatic event mapping (e.g., onToggle)
  • TypeScript support with proper types
  • Children/slots work automatically

3. Vue Wrappers (Vue-specific) ​

Vue 3 composition API wrappers:

vue
<script setup>
import VueButton from "./components/ag/Button/vue/VueButton.vue";
</script>

<template>
  <VueButton variant="primary">Click me</VueButton>
</template>

Benefits:

  • Native Vue syntax
  • Reactive props and events
  • Slot support
  • Full TypeScript support

Component Structure ​

Each component follows a consistent file structure:

Button/
├── core/
│   ├── _Button.ts          # Immutable canonical reference
│   ├── Button.ts           # Experimental/modifiable version
│   ├── Button.spec.ts      # Tests
│   └── README.md           # Documentation
├── react/
│   ├── ReactButton.tsx     # React wrapper
│   ├── index.ts            # Exports
│   └── ReactButton.spec.tsx
└── vue/
    ├── VueButton.vue       # Vue wrapper
    ├── index.ts            # Exports
    └── VueButton.spec.ts

Canonical vs Experimental Pattern ​

AgnosticUI uses a unique pattern to support both stability and experimentation:

_Component.ts (Canonical)

  • Immutable reference implementation
  • Follows official WAI-ARIA patterns
  • Upgrade-safe version
  • Source of truth for the component

Component.ts (Experimental)

  • Copy you can modify freely
  • Safe to experiment with
  • Your customizations live here
  • Can be reset to canonical version

This pattern allows AI tools to reference the canonical version while you experiment with the modifiable version.

Design Token System ​

AgnosticUI's theming is powered by CSS custom properties (CSS variables) organized into semantic tokens.

Token Philosophy ​

Semantic Naming Tokens describe purpose, not appearance:

  • --ag-primary not --ag-blue-500
  • --ag-text-primary not --ag-gray-900
  • --ag-radius-md not --ag-radius-6px

Theme Awareness All tokens automatically adapt to light/dark mode:

css
/* Light mode (default) */
:root {
  --ag-background-primary: #ffffff;
  --ag-text-primary: #111827;
}

/* Dark mode */
[data-theme="dark"] {
  --ag-background-primary: #010409;
  --ag-text-primary: #F0F6FC;
}

Token Categories ​

Spacing (--ag-space-*)

  • 10 levels from 0 to 2.5rem
  • Used for padding, margins, gaps

Colors (--ag-*)

  • Semantic: primary, secondary, danger, warning
  • Neutrals: 50-900 scale
  • Surfaces: primary, secondary, tertiary
  • Text: primary, secondary, muted

Typography (--ag-font-size-*, --ag-line-height-*)

  • Font sizes from xs to 5x
  • Line heights for different densities

Borders (--ag-radius-*)

  • Border radius from none to full
  • Consistent rounded corners

Shadows (--ag-shadow-*)

  • 6 elevation levels (xs to 2xl)
  • Subtle to dramatic effects

Motion (--ag-motion-*)

  • Timing values for transitions
  • Fast, medium, slow durations

Z-Index (--ag-z-index-*)

  • Layering for dropdowns, modals, toasts
  • Prevents z-index conflicts

Customizing Tokens ​

Override tokens in your own CSS:

css
:root {
  /* Brand colors */
  --ag-primary: #9333ea;
  --ag-focus: #a855f7;

  /* Spacing adjustments */
  --ag-space-4: 1.25rem;

  /* Border radius */
  --ag-radius-md: 0.5rem;
}

See the theming documentation for the complete token reference.

CLI Deep Dive ​

The AgnosticUI CLI manages the local-first workflow.

How ag init Works ​

When you run npx agnosticui-cli init:

  1. Framework Selection - Interactive prompt for React/Vue/Lit/Svelte
  2. Reference Library Setup - Extracts from tarball or NPM to ./agnosticui/
  3. Infrastructure Copy - Copies shared code to your components path:
    • /utils/ - Helper functions
    • /styles/ - Design tokens
    • /types/ - TypeScript definitions
    • /shared/ - Shared components
  4. Config Creation - Generates agnosticui.config.json
  5. TypeScript Configuration - Updates tsconfig.json with:
    • experimentalDecorators: true
    • useDefineForClassFields: false
  6. Dependency Installation - Installs framework dependencies
  7. Next Steps - Displays setup instructions

How ag add Works ​

When you run npx agnosticui-cli add button input:

  1. Validation - Checks components exist in reference library
  2. Duplicate Detection - Skips already-added components (unless --force)
  3. Component Copy - Copies component files to your project
  4. Dependency Resolution - Automatically detects and copies:
    • Shared dependencies (e.g., CloseButton)
    • Component dependencies (e.g., CopyButton → IconButton)
    • Recursive processing up to 10 levels deep
  5. Path Transformation - Adjusts import paths:
    • Strips .js extensions
    • Fixes relative paths based on nesting
  6. Config Update - Records added components in config
  7. Import Examples - Shows copy-paste import statements

Dependency Auto-Detection ​

The CLI intelligently scans component files for dependencies:

typescript
// CLI detects these imports
import { CloseButton } from '../../../shared/CloseButton';
import { IconButton } from '../../IconButton/core/IconButton';

// And automatically copies:
// - shared/CloseButton
// - IconButton (+ its dependencies)

This ensures all required code is available in your project.

Event System ​

AgnosticUI components emit custom events following web component standards.

Event Pattern ​

In Core Component:

typescript
export interface ButtonToggleEventDetail {
  pressed: boolean;
}

const event = new CustomEvent<ButtonToggleEventDetail>('toggle', {
  detail: { pressed: !this.pressed },
  bubbles: true,
  composed: true,
});
this.dispatchEvent(event);

In React:

typescript
<ReactButton onToggle={(event) => console.log(event.detail.pressed)} />

In Vue:

vue
<VueButton @toggle="(event) => handleToggle(event.detail.pressed)" />

In Lit/Vanilla:

typescript
button.addEventListener('toggle', (event) => {
  console.log(event.detail.pressed);
});

Standard vs Custom Events ​

Standard Events (click, focus, blur, etc.)

  • Handled normally in all frameworks
  • No special mapping needed

Custom Events (toggle, change, etc.)

  • Carry data in event.detail
  • React: Mapped to on[EventName] props by @lit/react
  • Vue: Handled via @event-name
  • Lit: Standard addEventListener

Accessibility First ​

All AgnosticUI components follow the WAI-ARIA Authoring Practices Guide (APG).

Key Accessibility Features ​

Semantic HTML

  • Proper element types (<button>, <input>, etc.)
  • Meaningful structure and hierarchy

ARIA Attributes

  • role, aria-label, aria-labelledby
  • aria-expanded, aria-controls, aria-haspopup
  • State attributes like aria-checked, aria-disabled

Keyboard Navigation

  • Tab navigation for interactive elements
  • Arrow keys for menus and lists
  • Escape to close dialogs and menus
  • Enter/Space for activation

Focus Management

  • Visible focus indicators via --ag-focus
  • Focus trap for modals and dialogs (using focus-trap)
  • Focus restoration when dialogs close

Screen Reader Support

  • Announcements for dynamic content
  • Proper labeling and descriptions
  • Live regions where appropriate

Testing Accessibility ​

AgnosticUI tests use jest-axe to catch accessibility issues:

typescript
import { axe } from 'jest-axe';

test('Button has no accessibility violations', async () => {
  const results = await axe(container);
  expect(results).toHaveNoViolations();
});

TypeScript Support ​

AgnosticUI is written in TypeScript and provides full type definitions.

Component Props Types ​

Each component exports its props type:

typescript
import type { ButtonProps } from './components/ag/Button/core/Button';
import type { ReactButtonProps } from './components/ag/Button/react/ReactButton';

Event Types ​

Custom events have typed detail payloads:

typescript
import type {
  ButtonToggleEvent,
  ButtonToggleEventDetail
} from './components/ag/Button/core/Button';

const handleToggle = (event: ButtonToggleEvent) => {
  console.log(event.detail.pressed); // TypeScript knows this exists
};

Required TypeScript Configuration ​

For Lit decorators to work:

json
{
  "compilerOptions": {
    "experimentalDecorators": true,
    "useDefineForClassFields": false
  }
}

Important: Don't add these to Vite's esbuild.tsconfigRaw - esbuild's decorator implementation is incomplete and breaks Lit's @property decorator.

Build and Distribution ​

Development Build ​

bash
cd v2/lib
npm run build

This:

  1. Syncs design tokens from theme-registry
  2. Runs Vite build for all components
  3. Copies styles to dist/
  4. Generates TypeScript declarations

Package Exports ​

AgnosticUI uses package.json exports for granular imports:

json
{
  "exports": {
    "./button": "./dist/button/core/Button.js",
    "./button/react": "./dist/button/react/ReactButton.js",
    "./button/vue": "./dist/button/vue/VueButton.vue",
    "./styles/*": "./dist/styles/*"
  }
}

This enables:

typescript
import 'agnosticui-core/button';
import { ReactButton } from 'agnosticui-core/button/react';
import 'agnosticui-core/styles/tokens.css';

Best Practices ​

Import from Local Components ​

Always import from your local components directory, not from the reference library:

typescript
// ✅ Correct
import { ReactButton } from './components/ag/Button/react/ReactButton';

// ❌ Wrong
import { ReactButton } from './agnosticui/lib/src/components/Button/react/ReactButton';

Use Design Tokens ​

Never hardcode colors, spacing, or other design values:

css
/* ✅ Correct */
.my-button {
  background: var(--ag-primary);
  padding: var(--ag-space-3);
  border-radius: var(--ag-radius-md);
}

/* ❌ Wrong */
.my-button {
  background: #3b82f6;
  padding: 12px;
  border-radius: 6px;
}

Customize Through Tokens First ​

Before modifying component code, try customizing through tokens:

css
:root {
  --ag-primary: #your-brand-color;
  --ag-radius-md: 0.5rem;
}

Only modify component code when tokens aren't sufficient.

Keep Reference Library Intact ​

The reference library in ./agnosticui/ should remain unmodified. It provides:

  • Context for AI tools
  • Reference for comparison
  • Source for re-adding components

Modifications go in ./src/components/ag/.

Version Control ​

Commit your components to git:

bash
git add src/components/ag/
git commit -m "Add Button and Input components"

Add reference library to .gitignore (optional):

agnosticui/

The CLI can recreate the reference library on other machines via ag init.

Framework-Specific Tips ​

React ​

Use React Wrappers:

typescript
import { ReactButton } from './components/ag/Button/react/ReactButton';

Event Handling: Custom events come through as props:

typescript
<ReactButton onToggle={(e) => console.log(e.detail)} />

Vue ​

Import .vue Files:

vue
import VueButton from './components/ag/Button/vue/VueButton.vue';

Configure Vite: Tell Vue to ignore ag-* custom elements:

typescript
vue({
  template: {
    compilerOptions: {
      isCustomElement: (tag) => tag.startsWith('ag-'),
    },
  },
})

Lit ​

Direct Web Component Usage:

typescript
import './components/ag/Button/core/Button.js';

Templates:

typescript
html`<ag-button variant="primary">Click</ag-button>`

Svelte ​

Import and Use:

svelte
<script>
  import './components/ag/Button/core/Button.js';
</script>

<ag-button variant="primary">Click</ag-button>

Svelte has excellent web component support out of the box.

What's Next? ​