Visor is a theming-first React component library built by Low Orbit 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.
In an empty directory, run:
npx @loworbitstudio/visor init --template nextjsThat single command scaffolds a complete, runnable Next.js App Router project pre-wired with Visor:
package.jsonwithnext,react, TypeScript, and@loworbitstudio/visor-core+@loworbitstudio/visor-theme-enginealready installed.app/layout.tsxwith the FOWT (Flash of Wrong Theme) prevention script inline in<head>,globals.cssimported, and the theme's declaredcolor-schemeapplied at the root — adark-onlybrand scaffolds<html className="dark">+color-scheme: darkso it renders dark even on a light-preference browser;light-onlyis the inverse;adaptivefollowsprefers-color-scheme.app/globals.cssgenerated from.visor.yamlvia the Visor Next.js adapter.app/page.tsx,tsconfig.json,next.config.ts,.gitignore— the full create-next-app baseline..lo/borealis.jsonstamp recording the Visor version that initialized the project.
Then start the dev server:
cd my-app && npm run devAdd your first component:
npx visor add buttonThat'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 nextjsonly scaffolds into empty directories — it refuses ifpackage.jsonalready exists so it never destructively overwrites in-flight work. For an existing app, use the manual setup below.
For non-Next.js projects, or to retrofit Visor into an existing app:
1. Initialize Visor
npx @loworbitstudio/visor initThis creates a visor.json in your project root with default path mappings:
{
"paths": {
"components": "components/ui",
"hooks": "hooks",
"lib": "lib"
}
}2. Import tokens into your global CSS
/* app/globals.css or src/index.css */
@import "@loworbitstudio/visor-core";3. Add your first component
npx visor add buttonAdd components one at a time or in bulk:
npx visor add input
npx visor add card
npx visor add button input label cardThe 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:
npx visor add --category form # Add all form components
npx visor add --category overlay # Add all overlay components11 compound components for data-heavy admin UIs. Add with --category admin:
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 |
| Infographic Bar | infographic-bar |
Continuous stat-card band with hairline dividers |
| Kbd | kbd |
Keyboard shortcut display |
| Matrix Table | matrix-table |
Members×roles boolean assignment grid |
| 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 |
19 full-page and section-level blocks. Add with --block:
npx visor add admin-dashboard --block
npx visor add hero-section --blockA block installs only the components it imports to render itself. Slot-driven
blocks (e.g. admin-shell) also declare suggested slot-fill components —
example fillers that are skipped by default so a slot-only compose stays lean.
Pull them with --with-suggested:
npx visor add admin-shell --block # hard deps only (no Radix)
npx visor add admin-shell --block --with-suggested # + breadcrumb, dropdown-menu, sidebar| 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 |
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
npx visor add use-boolean
npx visor add use-debounce
npx visor add use-slide-engineWhen 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:
/* 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:
// 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 is Visor's core differentiator. Every component references CSS custom properties — never hard-coded values. Swap the token values and the entire UI follows.
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.
Visor ships with a dark theme out of the box. Apply it by adding .theme-dark to your root element:
<html class="theme-dark">Override any token after your @import statement — no forking required:
/* 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;
}/* 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);
}// app/layout.tsx
export default function RootLayout({ children }) {
return (
<html lang="en" className="theme-brand">
<body>{children}</body>
</html>
)
}Define your theme in a YAML file and generate framework-specific CSS:
# .visor.yaml
name: my-brand
version: 1
colors:
primary: "#6366f1"# 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 docsRegister a theme in the Visor docs site in one command:
# 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-brandOr scaffold a complete themed project:
npx @loworbitstudio/visor init --template nextjsAlready 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.
FOWT prevention covers two orthogonal axes. Add the script(s) you need as blocking <script> tags in your <head>, before any stylesheets.
Mode axis (dark/light). Reads localStorage('visor-theme'), falls back to prefers-color-scheme, and sets .dark or .light on <html> before first paint:
import { FOWT_SCRIPT } from '@loworbitstudio/visor-theme-engine/fowt';
// In your layout.tsx <head>:
<script>{FOWT_SCRIPT}</script>Theme-identity axis (palette). For apps that switch between N registered themes at runtime, generateThemeFowtScript validates the stored theme name against an allowlist (falling back to a default), stamps it on <html>, and enables exactly the matching inlined <style data-theme-css>:
import { generateThemeFowtScript } from '@loworbitstudio/visor-theme-engine/fowt';
const themeScript = generateThemeFowtScript({
themes: ['entr', 'space'], // registered-theme allowlist
defaultTheme: 'entr', // fallback when nothing valid is stored
// storageKey: 'visor-theme-name', // optional, this is the default
// attribute: 'data-theme-name', // optional, this is the default
});
// In your layout.tsx <head>, AFTER the inlined <style data-theme-css="..."> tags:
<script>{themeScript}</script>Both scripts are ES5-safe and can live in the same <head> — the mode axis toggles a class, the palette axis toggles which theme stylesheet is active.
@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 */Visor's distributed CSS uses CSS Cascade Layers 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:
@layer visor-base, visor-primitives, visor-semantic, visor-brand, visor-adaptive, visor-bridge;| Layer | Source | Purpose |
|---|---|---|
visor-base |
@loworbitstudio/visor-core/reset (opt-in), generated themes (visor theme apply --adapter nextjs) |
Element baseline — binds theme tokens to html/body and normalises UA element defaults |
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-brand |
Generated themes (overrides pass-through) |
Brand asset vars (--brand-*) |
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. visor-baseis deliberately lowest. Your own unlayeredbody { ... }and every component.module.css(which uses no@layerat all) beat it unconditionally. Author-origin still beats the browser's UA stylesheet regardless of layer, so the reset does its job without ever fighting your code.- 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}-themeclass still wins on selector specificity but its rules participate invisor-adaptiveso they coexist cleanly with generated themes.
Re-run the CLI with --overwrite to pull the latest upstream version:
npx visor add button --overwriteBecause you own the files, the CLI shows a diff before overwriting. If you've customized the component, use git to merge:
- Commit your customizations.
- Run
npx visor add button --overwrite. - Use
git diffto review what changed. - Merge your customizations into the updated version.
- Commit the result.
Token updates are standard npm updates:
npm update @loworbitstudio/visor-coreToken updates propagate automatically to all components. No component files change.
# 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 <component> # Add a component, hook, or lib entry
npx @loworbitstudio/visor add <c1> <c2> <c3> # Add multiple at once
npx @loworbitstudio/visor add --category <name> # Add all items in a category
npx @loworbitstudio/visor add <component> --block # Add a block (hard deps only)
npx @loworbitstudio/visor add <block> --block --with-suggested # + a block's suggested slot-fill components
npx @loworbitstudio/visor add <component> --overwrite # Update an existing component
npx @loworbitstudio/visor list # List all available components
npx @loworbitstudio/visor list --category <name> # List by category
npx @loworbitstudio/visor diff [component] # Show local vs. registry differences
npx @loworbitstudio/visor suggest --for "<use case>" # Find components for a use case
npx @loworbitstudio/visor suggest --for "<use case>" --json # JSON output (for AI agents)
# Themes
npx @loworbitstudio/visor theme apply <file> # Generate CSS from .visor.yaml
npx @loworbitstudio/visor theme apply <file> --adapter nextjs # Next.js adapter
npx @loworbitstudio/visor theme apply <file> --adapter nextjs --scope-prefix 'body.my-theme' # Body-class scoped output
npx @loworbitstudio/visor theme apply <file> --adapter fumadocs # fumadocs adapter
npx @loworbitstudio/visor theme apply <file> --adapter deck # Deck adapter
npx @loworbitstudio/visor theme validate <file> # 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 <file> # Register theme in the docs site
npx @loworbitstudio/visor theme unregister <slug> # Remove a theme from the docs site
npx @loworbitstudio/visor theme sync # Re-generate CSS for all themes
# Fonts
npx @loworbitstudio/visor fonts add <path> --org <name> # Upload woff2 to Visor Fonts CDN
# Render fidelity
npx @loworbitstudio/visor render <component> --theme <slug> --mode <light|dark> # Render one real component to a PNG (serverless)
npx @loworbitstudio/visor render doc-nav --theme space --mode dark # Real tokens + real theme CSS + real component
npx @loworbitstudio/visor render button --theme neutral --state hover # Capture an interactive statevisor render is a per-component render-fidelity harness: it composes the real emitted tokens + real per-theme CSS (with correct @layer/mode scoping) + the real esbuild-bundled component, then screenshots it with Playwright — no next dev. A computed-style probe confirms the themed surface resolved to its mapped value, not the raw primitive. playwright and esbuild are lazy/optional deps (not bundled into the CLI); the command prompts to install them if missing.
All commands support --json for structured output (useful for AI agents and scripts).
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 recipes — Recipe files in patterns/ document how components combine for common use cases (form with validation, dashboard layout, data table with filters). Visor calls these composition recipes, distinct from a design-language pattern.
See docs/ai-consumability.md for the full spec.
- React + TypeScript
- CSS Modules + CSS custom properties (no Tailwind, no CSS-in-JS)
- CVA for variant management
- Radix UI for accessible primitives
- Phosphor Icons
- Vitest + React Testing Library for testing
- fumadocs for the documentation site
Full documentation, component previews, and a props reference are available at:
- Kaiah — AI-powered marketing platform
- Blacklight — Music industry intelligence tool
Using Visor in your project? Open a PR to add it here.
See CONTRIBUTING.md for guidelines on submitting components, token changes, and bug fixes.
To develop locally:
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 themesEvery 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/<branch-slug>.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:
node scripts/generate-changeset.mjs
# or the standard interactive way:
npm run changesetOn-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:
git push --no-verifyAuto-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.
visor/
├── components/ui/ # Component source + .visor.yaml metadata
├── hooks/ # Hook source (registry entries)
├── lib/ # Utility source (registry entries)
├── patterns/ # Composition recipes (.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
Every visor_* Flutter widget is audited against the Flutter Widget Quality Contract — 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.
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.
/lo-visor-publish (see .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.node scripts/visor-publish-status.mjs
coordinate <visor-PR> <themes-PR>— single-confirmation cross-repo release for the case where a feature spans Visor + visor-themes-private and both must ship together.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 for the full audit.
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 and docs/wisdom/W029-vi-ticket-publish-governance.md.
Visor is free and open-source, built and maintained by Low Orbit 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.
See LICENSE for details.
Built by Low Orbit Studio — Brooklyn, NY.
