https://github.com/low-orbit-studio/visor
https://github.com/low-orbit-studio/visor
component-library css-modules design-system nextjs oklch react shadcn-style theming visor
Last synced: 9 days ago
JSON representation
- Host: GitHub
- URL: https://github.com/low-orbit-studio/visor
- Owner: low-orbit-studio
- License: mit
- Created: 2026-03-19T19:56:29.000Z (2 months ago)
- Default Branch: main
- Last Pushed: 2026-05-08T02:09:54.000Z (23 days ago)
- Last Synced: 2026-05-08T02:42:11.693Z (23 days ago)
- Topics: component-library, css-modules, design-system, nextjs, oklch, react, shadcn-style, theming, visor
- Language: TypeScript
- Homepage: https://visor-docs.vercel.app
- Size: 25.7 MB
- Stars: 0
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- Changelog: CHANGELOG.md
- Contributing: CONTRIBUTING.md
- License: LICENSE
- Code of conduct: CODE_OF_CONDUCT.md
- Security: SECURITY.md
- Governance: GOVERNANCE.md
- Roadmap: docs/roadmap.md
- Maintainers: MAINTAINERS.md
Awesome Lists containing this project
README
---
## What is Visor?
Visor is a theming-first React component library built by [Low Orbit Studio](https://loworbit.studio). It uses a two-layer distribution model that gives you full control over your components while keeping design consistency effortless:
**Layer 1 — Components (copy-and-own).** Run `npx visor add button` and the source files are copied directly into your project. You own them. Edit them freely. No runtime dependency on Visor.
**Layer 2 — Tokens (`@loworbitstudio/visor-core`).** The only npm package. It provides all the CSS custom properties that Visor components reference. Update the package and design changes cascade to every component automatically — without touching a single component file.
This model is inspired by shadcn/ui's copy-and-own approach, combined with a shared token layer that keeps multi-project consistency without locking you in.
---
## Quick Start
### One command. Runnable Next.js app. Borealis-native.
In an empty directory, run:
```sh
npx @loworbitstudio/visor init --template nextjs
```
That single command scaffolds a complete, runnable Next.js App Router project pre-wired with Visor:
- `package.json` with `next`, `react`, TypeScript, and `@loworbitstudio/visor-core` + `@loworbitstudio/visor-theme-engine` already installed.
- `app/layout.tsx` with the FOWT (Flash of Wrong Theme) prevention script inline in `` and `globals.css` imported.
- `app/globals.css` generated from `.visor.yaml` via the Visor Next.js adapter.
- `app/page.tsx`, `tsconfig.json`, `next.config.ts`, `.gitignore` — the full create-next-app baseline.
- `.lo/borealis.json` stamp recording the Visor version that initialized the project.
Then start the dev server:
```sh
cd my-app && npm run dev
```
Add your first component:
```sh
npx visor add button
```
That's it. The component source lands in your project and you own it. No FOWT flash, no missing config, no second setup step.
> **Heads up:** `visor init --template nextjs` only scaffolds into empty directories — it refuses if `package.json` already exists so it never destructively overwrites in-flight work. For an existing app, use the manual setup below.
### Manual setup (non-Next.js or retrofit)
For non-Next.js projects, or to retrofit Visor into an existing app:
**1. Initialize Visor**
```sh
npx @loworbitstudio/visor init
```
This creates a `visor.json` in your project root with default path mappings:
```json
{
"paths": {
"components": "components/ui",
"hooks": "hooks",
"lib": "lib"
}
}
```
**2. Import tokens into your global CSS**
```css
/* app/globals.css or src/index.css */
@import "@loworbitstudio/visor-core";
```
**3. Add your first component**
```sh
npx visor add button
```
---
## Adding Components
Add components one at a time or in bulk:
```sh
npx visor add input
npx visor add card
npx visor add button input label card
```
### Available Components
The registry ships 88+ UI components across 6 categories, plus admin compounds, blocks, and hooks.
**Form (24)**
`button` · `calendar` · `checkbox` · `combobox` · `date-picker` · `field` · `fieldset` · `file-upload` · `form` · `input` · `label` · `number-input` · `otp-input` · `password-input` · `phone-input` · `radio-group` · `search-input` · `select` · `slider` · `slider-control` · `switch` · `tag-input` · `textarea` · `toggle-group`
**Data Display (12)**
`accordion` · `avatar` · `carousel` · `code-block` · `collapsible` · `heading` · `image` · `progress` · `separator` · `skeleton` · `text` · `timeline`
**Navigation (5)**
`breadcrumb` · `command` · `navbar` · `pagination` · `stepper`
**Overlay (7)**
`context-menu` · `dialog` · `fullscreen-overlay` · `hover-card` · `lightbox` · `menubar` · `popover`
**Feedback (6)**
`alert` · `banner` · `chart` · `table` · `toast` · `tooltip`
**Layout (9)**
`badge` · `box` · `card` · `container` · `grid` · `inline` · `sheet` · `sidebar` · `stack`
The five primitives `box`, `container`, `grid`, `inline`, and `stack` are token-typed layout building blocks: all spacing, surface, and radius props accept only Visor token names — off-system values are TypeScript errors. Pair them with `Card`, `Sheet`, and `Sidebar` for full-page chrome.
Add an entire category at once:
```sh
npx visor add --category form # Add all form components
npx visor add --category overlay # Add all overlay components
```
### Admin Components
10 compound components for data-heavy admin UIs. Add with `--category admin`:
```sh
npx visor add --category admin
```
| Component | CLI Name | Description |
|-----------|----------|-------------|
| Activity Feed | `activity-feed` | Timestamped event stream |
| Bulk Action Bar | `bulk-action-bar` | Floating bar for multi-select actions |
| Confirm Dialog | `confirm-dialog` | Destructive action confirmation modal |
| Data Table | `data-table` | Sortable, filterable table with pagination |
| Empty State | `empty-state` | Zero-data placeholder with CTA |
| Filter Bar | `filter-bar` | Composable filter chip row |
| Kbd | `kbd` | Keyboard shortcut display |
| Page Header | `page-header` | Title + actions header for admin pages |
| Stat Card | `stat-card` | KPI metric card with trend |
| Status Badge | `status-badge` | Semantic status indicator |
### Blocks
19 full-page and section-level blocks. Add with `--block`:
```sh
npx visor add admin-dashboard --block
npx visor add hero-section --block
```
| Block | CLI Name | Category |
|-------|----------|----------|
| Admin Dashboard | `admin-dashboard` | Admin |
| Admin Detail Drawer | `admin-detail-drawer` | Admin |
| Admin List Page | `admin-list-page` | Admin |
| Admin Settings Page | `admin-settings-page` | Admin |
| Admin Shell | `admin-shell` | Admin |
| Admin Tabbed Editor | `admin-tabbed-editor` | Admin |
| Admin Wizard | `admin-wizard` | Admin |
| CTA Section | `cta-section` | Marketing |
| Features Grid | `features-grid` | Marketing |
| Footer Section | `footer-section` | Marketing |
| Hero Section | `hero-section` | Marketing |
| Pricing Section | `pricing-section` | Marketing |
| Steps Section | `steps-section` | Marketing |
| Testimonial Section | `testimonial-section` | Marketing |
| Login Form | `login-form` | Auth |
| Configuration Panel | `configuration-panel` | Configuration |
| Design System Deck | `design-system-deck` | Documentation |
| Design System Specimen | `design-system-specimen` | Documentation |
| Sphere Playground | `sphere-playground` | Visual |
### Available Hooks
**General (10)**
`use-boolean` · `use-click-outside` · `use-currency` · `use-debounce` · `use-focus-trap` · `use-intersection-observer` · `use-keyboard-shortcut` · `use-local-storage` · `use-media-query` · `use-previous`
**Deck (4)**
`use-intersection-animation` · `use-keyboard-nav` · `use-slide-engine` · `use-wheel-nav`
```sh
npx visor add use-boolean
npx visor add use-debounce
npx visor add use-slide-engine
```
---
## How It Works
When you run `npx visor add button`, two files land in your project:
```
your-project/
├── components/
│ └── ui/
│ └── button/
│ ├── button.tsx ← React component (yours to edit)
│ └── button.module.css ← Component styles (yours to edit)
└── lib/
└── utils.ts ← cn() helper, added once and shared
```
Components use CSS Modules for scoped class names and CSS custom properties from the tokens package for all design values:
```css
/* button.module.css */
.base {
border-radius: var(--radius-md);
font-size: var(--text-sm);
}
.variantDefault {
background-color: var(--interactive-primary-bg);
color: var(--interactive-primary-text);
}
```
Variants are managed with [CVA](https://cva.style):
```tsx
// button.tsx
const buttonVariants = cva(styles.base, {
variants: {
variant: {
default: styles.variantDefault,
secondary: styles.variantSecondary,
},
size: {
sm: styles.sizeSm,
md: styles.sizeMd,
},
},
defaultVariants: { variant: "default", size: "md" },
})
```
---
## Theming
Theming is Visor's core differentiator. Every component references CSS custom properties — never hard-coded values. Swap the token values and the entire UI follows.
### The 3-Tier Token Architecture
```
Tier 1: Primitives Tier 2: Semantic Tier 3: Adaptive
--color-gray-900 ──→ --text-primary ──→ :root { --text-primary }
--color-gray-50 ──→ --surface-page ──→ .theme-dark { ... }
--radius-lg ──→ --border-default
```
Components only reference Tier 2 (semantic) tokens. This means overriding a single semantic token updates every component that uses it.
### Dark Mode
Visor ships with a dark theme out of the box. Apply it by adding `.theme-dark` to your root element:
```html
```
### Overriding Tokens
Override any token after your `@import` statement — no forking required:
```css
/* globals.css */
@import "@loworbitstudio/visor-core";
:root {
/* Rebrand the primary color across the entire system */
--interactive-primary-bg: #6366f1;
--interactive-primary-bg-hover: #4f46e5;
}
.theme-dark {
--interactive-primary-bg: #818cf8;
}
```
### Creating a Custom Theme
```css
/* styles/theme-brand.css */
.theme-brand {
--surface-page: #0a0a14;
--surface-card: #12121f;
--text-primary: #f0f0ff;
--text-secondary: #a0a0c0;
--interactive-primary-bg: #6366f1;
--interactive-primary-text: #ffffff;
--border-default: rgba(255, 255, 255, 0.1);
}
```
```tsx
// app/layout.tsx
export default function RootLayout({ children }) {
return (
{children}
)
}
```
### Creating a Theme from `.visor.yaml`
Define your theme in a YAML file and generate framework-specific CSS:
```yaml
# .visor.yaml
name: my-brand
version: 1
colors:
primary: "#6366f1"
```
```bash
# Generate Next.js globals.css with @layer support
npx @loworbitstudio/visor theme apply .visor.yaml --adapter nextjs
# Generate fumadocs bridge tokens
npx @loworbitstudio/visor theme apply .visor.yaml --adapter fumadocs
# Generate scoped deck CSS
npx @loworbitstudio/visor theme apply .visor.yaml --adapter deck
# Generate docs-site CSS (class-scoped, includes fumadocs bridge)
npx @loworbitstudio/visor theme apply .visor.yaml --adapter docs
```
Register a theme in the Visor docs site in one command:
```bash
# Creates CSS file, updates globals.css and theme-config.ts
npx @loworbitstudio/visor theme register .visor.yaml --group "Client"
# Preview changes without writing
npx @loworbitstudio/visor theme register .visor.yaml --group "Client" --dry-run
# Remove a theme
npx @loworbitstudio/visor theme unregister my-brand
```
Or scaffold a complete themed project:
```bash
npx @loworbitstudio/visor init --template nextjs
```
### FOWT Prevention
> Already wired automatically when you use `npx @loworbitstudio/visor init --template nextjs`. The steps below are only needed for manual setups or non-Next.js apps.
Prevent flash of wrong theme by adding a blocking script to your ``:
```typescript
import { FOWT_SCRIPT } from '@loworbitstudio/visor-theme-engine/fowt';
// In your layout.tsx :
{FOWT_SCRIPT}
```
### Importing Specific Token Layers
```css
@import "@loworbitstudio/visor-core/primitives"; /* Tier 1: raw values */
@import "@loworbitstudio/visor-core/semantic"; /* Tier 2: purpose-named */
@import "@loworbitstudio/visor-core/themes/light"; /* Tier 3: light theme */
@import "@loworbitstudio/visor-core/themes/dark"; /* Tier 3: dark theme */
```
### CSS Layer Architecture
Visor's distributed CSS uses [CSS Cascade Layers](https://developer.mozilla.org/en-US/docs/Web/CSS/@layer) so generated themes win the cascade without consumer intervention.
Every shipped `dist/*.css` file declares the same layer order and wraps its content in the matching tier:
```css
@layer visor-primitives, visor-semantic, visor-adaptive, visor-bridge;
```
| Layer | Source | Purpose |
| --- | --- | --- |
| `visor-primitives` | `@loworbitstudio/visor-core/primitives` | Raw token values (colors, spacing, type) |
| `visor-semantic` | `@loworbitstudio/visor-core/semantic` | Purpose-named tokens (`--text-primary`, `--surface-card`) |
| `visor-adaptive` | `@loworbitstudio/visor-core/themes/*`, generated themes (`visor theme apply --adapter nextjs`) | Light/dark-aware tokens, generated theme overrides |
| `visor-bridge` | Framework integrations (e.g. fumadocs) | Maps Visor tokens onto framework-native variables |
**Cascade rules at a glance:**
- Per the CSS spec, **unlayered styles always beat layered styles**. So your bare `:root { ... }` overrides written after `@import "@loworbitstudio/visor-core"` continue to win — that pattern still works as documented above.
- **Generated themes win over visor-core defaults.** Both sit in `@layer visor-adaptive`, and last-loaded wins within a layer, so importing a generated theme after visor-core gives the theme its expected priority.
- **Stock themes ship layered too.** When you import `@loworbitstudio/visor-core/themes/blackout` (or any other stock theme), the `.{slug}-theme` class still wins on selector specificity but its rules participate in `visor-adaptive` so they coexist cleanly with generated themes.
---
## Updating
### Updating a Component
Re-run the CLI with `--overwrite` to pull the latest upstream version:
```sh
npx visor add button --overwrite
```
Because you own the files, the CLI shows a diff before overwriting. If you've customized the component, use git to merge:
1. Commit your customizations.
2. Run `npx visor add button --overwrite`.
3. Use `git diff` to review what changed.
4. Merge your customizations into the updated version.
5. Commit the result.
### Updating Tokens
Token updates are standard npm updates:
```sh
npm update @loworbitstudio/visor-core
```
Token updates propagate automatically to all components. No component files change.
---
## CLI Reference
```sh
# Setup
npx @loworbitstudio/visor init # Create visor.json config
npx @loworbitstudio/visor init --template nextjs # Initialize with Next.js template
# Components
npx @loworbitstudio/visor add # Add a component, hook, or lib entry
npx @loworbitstudio/visor add # Add multiple at once
npx @loworbitstudio/visor add --category # Add all items in a category
npx @loworbitstudio/visor add --block # Add a block
npx @loworbitstudio/visor add --overwrite # Update an existing component
npx @loworbitstudio/visor list # List all available components
npx @loworbitstudio/visor list --category # List by category
npx @loworbitstudio/visor diff [component] # Show local vs. registry differences
npx @loworbitstudio/visor suggest --for "" # Find components for a use case
npx @loworbitstudio/visor suggest --for "" --json # JSON output (for AI agents)
# Themes
npx @loworbitstudio/visor theme apply # Generate CSS from .visor.yaml
npx @loworbitstudio/visor theme apply --adapter nextjs # Next.js adapter
npx @loworbitstudio/visor theme apply --adapter nextjs --scope-prefix 'body.my-theme' # Body-class scoped output
npx @loworbitstudio/visor theme apply --adapter fumadocs # fumadocs adapter
npx @loworbitstudio/visor theme apply --adapter deck # Deck adapter
npx @loworbitstudio/visor theme validate # Validate a .visor.yaml theme
npx @loworbitstudio/visor theme export [file] # Export theme to YAML/JSON
npx @loworbitstudio/visor theme extract # Extract .visor.yaml from existing CSS
npx @loworbitstudio/visor theme register # Register theme in the docs site
npx @loworbitstudio/visor theme unregister # Remove a theme from the docs site
npx @loworbitstudio/visor theme sync # Re-generate CSS for all themes
# Fonts
npx @loworbitstudio/visor fonts add --org # Upload woff2 to Visor Fonts CDN
```
All commands support `--json` for structured output (useful for AI agents and scripts).
---
## AI Agent Consumability
Visor includes structured metadata that makes it easy for AI agents to discover, understand, and compose components without reading source code.
**Per-component metadata** — Each component has a `.visor.yaml` file alongside its source with props, variants, slots, dependencies, usage examples, and "when to use" / "when not to use" guidance.
**Registry manifest** — `visor-manifest.json` is auto-generated during build, aggregating all component metadata (including auto-extracted CSS tokens) into a single file an agent can load.
**Composition patterns** — Pattern files in `patterns/` document how components combine for common use cases (form with validation, dashboard layout, CRUD table).
See [docs/ai-consumability.md](docs/ai-consumability.md) for the full spec.
---
## Stack
- **React + TypeScript**
- **CSS Modules** + CSS custom properties (no Tailwind, no CSS-in-JS)
- **[CVA](https://cva.style)** for variant management
- **[Radix UI](https://radix-ui.com)** for accessible primitives
- **[Phosphor Icons](https://phosphoricons.com)**
- **[Vitest](https://vitest.dev) + [React Testing Library](https://testing-library.com/react)** for testing
- **[fumadocs](https://fumadocs.vercel.app)** for the documentation site
---
## Documentation
Full documentation, component previews, and a props reference are available at:
**[visor.loworbit.studio](https://visor.loworbit.studio)**
---
## Built with Visor
- **[Kaiah](https://github.com/low-orbit-studio/kaiah)** — AI-powered marketing platform
- **[Blacklight](https://github.com/low-orbit-studio/blacklight)** — Music industry intelligence tool
Using Visor in your project? [Open a PR](https://github.com/low-orbit-studio/visor/edit/main/README.md) to add it here.
---
## Contributing
See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines on submitting components, token changes, and bug fixes.
To develop locally:
```sh
git clone https://github.com/loworbit/visor.git
cd visor
npm install
npm test # Run tests
npm run typecheck # Type check
npm run lint # Lint
npm run build # Build all packages
npm run docs:dev # Start docs site
npm run widgetbook:dev # Start Flutter widgetbook preview (requires Flutter SDK)
npm run themes:apply-flutter # Regenerate packages/visor_themes/ for all 11 themes
```
### Changesets (every shipping-package change)
Every PR that touches shipping-package source needs a `.changeset/*.md` file. The `Changeset Gate` workflow blocks merge if one is missing or malformed.
**Automatic generation (recommended).** The pre-push git hook runs `scripts/generate-changeset.mjs` before every push. If the diff touches a shipping path (any directory listed in `changeset-paths.json` — `components/`, `blocks/`, `hooks/`, `lib/`, `registry/`, `themes/`, `patterns/`, `assets/`, or the `src/`/`lib/` trees of the published packages) and no operator-authored changeset exists yet, Claude will write `.changeset/.md` and stage it automatically. The same `changeset-paths.json` drives the CI changeset gate, so the local hook and CI stay in sync.
Requirements: `claude` CLI must be installed globally (`npm install -g @anthropic-ai/claude-code`). If it's not available, the hook prints a warning and the push proceeds normally.
**Manual generation.** Run at any time:
```sh
node scripts/generate-changeset.mjs
# or the standard interactive way:
npm run changeset
```
**On-demand via Claude Code.** The `/lo-changeset` skill at `.claude/skills/lo-changeset/SKILL.md` wraps the same script:
```
/lo-changeset
```
**Bypass the hook.** Skip changeset generation for a push:
```sh
git push --no-verify
```
**Auto-generated marker.** Generated changesets include `# generated-by: lo-changeset` in their YAML frontmatter. If you edit the changeset and remove that marker, it becomes operator-authored — the hook will not overwrite it on subsequent pushes. Operator overrides always win.
**Failure handling.** If `claude` fails for any reason, the hook exits 0 and the push proceeds. Run `npm run changeset` manually if you need a minor/major bump and the auto-generation failed.
**Prompt source.** `scripts/changeset-prompt.md` contains the bump-type rules and output format. Edit it to tune the AI's behavior.
### Repository Structure
```
visor/
├── components/ui/ # Component source + .visor.yaml metadata
├── hooks/ # Hook source (registry entries)
├── lib/ # Utility source (registry entries)
├── patterns/ # Composition patterns (.visor-pattern.yaml)
├── registry/ # Registry schema and definitions
└── packages/
├── cli/ # @loworbitstudio/visor CLI + manifest builder
├── tokens/ # @loworbitstudio/visor-core npm package
├── visor-flutter/ # visor_core Flutter package (pub.dev)
├── visor_themes/ # All 11 Visor ThemeData — generated, do not edit
├── widgetbook/ # Flutter widgetbook preview app
└── docs/ # fumadocs documentation site
```
### Flutter widget quality
Every `visor_*` Flutter widget is audited against the [Flutter Widget Quality Contract](./docs/flutter-widget-quality-contract.md) — a tiered checklist (Required / Recommended / Stretch) covering tokens, semantics, touch targets, reduce-motion, RTL, tests, and a11y matchers. Required-tier compliance gates a widget being marked production-ready.
---
## Operator workflows
Day-to-day publishing is automatic for the three public npm packages — `.changeset/*.md` files written on each PR drive the bumps, and `release.yml` opens a "Version Packages" PR that publishes on merge. `@low-orbit-studio/visor-themes-private` still auto-versions on its own merges. The commands below surface only for the rare cases where a human is in the loop: health checks and cross-repo coordinated releases.
### Publishing health and coordinated releases
`/lo-visor-publish` (see [`.claude/skills/lo-visor-publish/SKILL.md`](./.claude/skills/lo-visor-publish/SKILL.md)) has two modes:
- **`status`** — read-only drift report across all 4 publishable artifacts. Non-zero exit on drift, so it can gate other workflows.
```bash
node scripts/visor-publish-status.mjs
```
- **`coordinate `** — single-confirmation cross-repo release for the case where a feature spans Visor + visor-themes-private and both must ship together.
```bash
node scripts/visor-publish-coordinate.mjs 369 2 --dry-run # preview only
node scripts/visor-publish-coordinate.mjs 369 2 # live
```
The skill itself contains no publish logic. Each repo's existing CI (`release.yml` on the Visor side, themes-private's `publish.yml`) remains the source of truth for what publishes. See [`docs/audits/publish-automation.md`](./docs/audits/publish-automation.md) for the full audit.
### Publish-gate audit (PR comment governance)
When the `visor-publish-smoke` workflow detects drift between the source on `main` and the latest published `@loworbitstudio/visor` tarball, the audit step maps each drifted primitive back to the PR that landed it and posts a comment there — so "merged" eventually catches up with "shipped." Uses the built-in `GITHUB_TOKEN`, no extra secrets required. Run it locally with `npm run audit:publish`. Full background in [`CLAUDE.md` § Publish Gate](./CLAUDE.md#publish-gate) and [`docs/wisdom/W029-vi-ticket-publish-governance.md`](./docs/wisdom/W029-vi-ticket-publish-governance.md).
---
## Sustainability
Visor is free and open-source, built and maintained by [Low Orbit Studio](https://loworbit.studio). If it's useful to you, here's how to support it:
- **Use it and share it** — the best support is adoption and word of mouth.
- **Contribute** — bug reports, PRs, and Discussions participation all help.
- **Hire us** — Low Orbit Studio takes on product and design system work. [Get in touch](https://loworbit.studio).
---
## License
See [LICENSE](LICENSE) for details.
---
Built by [Low Orbit Studio](https://loworbit.studio) — Brooklyn, NY.