stargazer
Component Explorer for Astro
Stargaze your components. Zero heavy deps, no throwaway pages. Just Astro.
Why Stargazer?
If you've ever built a component-based website (Astro, Next.js, Svelte, you name it), you've probably done this:
- Build a component
- Move on to the next one
- Three weeks later… "wait, what did that Hero look like again?"
- Create a random test page, import the component, check it, delete the page
- Repeat forever
We all do it. It works, but it's not great.
Stargazer started because I wanted something better. Just list your components in one config file and get a full visual catalog with viewport testing, zoom, and dark mode toggle. It runs inside your existing Astro dev server. No extra tools, no extra build step, no heavy install.
Inspired By
This was built on the ideas of some solid tools that already exist:
- Storybook — the OG, massive ecosystem, addon library for days
- Histoire — beautiful, great for Vue and Svelte
- Ladle — lightweight Storybook alternative for React
All great tools. But none of them are built for Astro, and most come with setup overhead that felt like overkill for what I needed.
Comparison
| Storybook | Histoire | Astro Stargazer | |
|---|---|---|---|
| Install size | ~50MB+ | ~15MB | ~0.5MB |
| Config per component | 1 .stories file each | 1 .story file each | 1 file total |
| Extra dev server | Yes | Yes | No, uses Astro's |
| Learning curve | Decorators, args, controls | Story syntax | Just a list of paths |
| Astro-native | ❌ | ❌ | ✅ |
This isn't a Storybook replacement. If you need interactive controls, docs generation, or a huge addon ecosystem, Storybook is the right choice. Stargazer is for when you just want to see your components quickly.
Framework components work too. Astro supports React, Vue, Svelte, Solid and others via islands. If your
.astrocomponent imports a React button or a Vue card, Stargazer previews it exactly as it would render in your real page — hydration and all.
Requirements
| Requirement | Version |
|---|---|
| Astro | ≥ 4.9.0 |
| Node.js | ≥ 18.0.0 |
Framework components (React, Vue, Svelte, Solid…) work out of the box if you already have them set up in your Astro project — no extra config needed.
Install
Option A — astro add (recommended)
npx astro add astro-stargazer This automatically updates your astro.config.mjs.
Option B — Manual
npm install astro-stargazer Config Setup
1. Add to your Astro config
// astro.config.mjs
import { defineConfig } from 'astro/config';
import stargazer from 'astro-stargazer';
export default defineConfig({
integrations: [stargazer()]
}); 2. Create your config
Use defineConfig from astro-stargazer for full IDE
autocomplete (zero runtime cost):
// stargazer.config.ts
import { defineConfig } from 'astro-stargazer';
export default defineConfig({
mode: 'auto',
defaultLayout: 'main',
layouts: { main: './src/layouts/Layout.astro' },
}); Or run the setup wizard to generate it interactively:
npx create-stargazer Or list components manually with mode: 'files':
import { defineConfig } from 'astro-stargazer';
export default defineConfig({
mode: 'files',
components: [
{ name: 'Hero', path: './src/components/Hero.astro', description: 'Landing hero section' },
{ name: 'Footer', path: './src/components/Footer.astro' },
],
}); That's it. No stories, no decorators, no extra syntax.
Run
npm run dev
Open /demo in your browser — check your
terminal for the exact port (usually 4321). 🔭
Works on macOS, Linux and Windows — pure Node.js, no native binaries.
Configuration Reference
Create stargazer.config.ts (or .js / .mjs) in your project root:
import type { StargazerConfig } from 'astro-stargazer';
const config: StargazerConfig = { ... };
export default config; Options
| Option | Type | Default | Description |
|---|---|---|---|
mode | 'files' | 'auto' | 'auto' | How components are discovered |
exclude | string[] | ['src/layouts','src/pages','src/styles'] | Paths to exclude (auto mode only) |
defaults | Record<string, unknown> | {} | Global props applied to all components |
defaultLayout | string | undefined | Key from layouts map to use by default |
layoutMap | Record<string, string> | undefined | Map directory paths to layout keys (auto/files mode) |
layouts | Record<string, string> | {} | Map of layout names to file paths |
components | StargazerComponent[] | [] | Components to preview (files mode only) |
scanDir | string | './src' | Directory to scan (auto mode only) |
base | string | '/demo' | URL path for Stargazer |
buildable | boolean | false | Include in production builds (default: dev-only) |
darkMode | false | DarkModeConfig | {} (enabled) | Dark mode toggle, set to false to hide |
Component Entry
Field Type Required Description namestring✅ Display name in the index pathstring✅ Path to .astro file (relative to project root) descriptionstring❌ Short description shown on the index card layoutstring❌ Layout key, overrides defaultLayout propsRecord<string, unknown>❌ Props, overrides defaults variantsStargazerVariant[]❌ Multiple prop combinations for the same component categorystring❌ Custom category label (default: detected from folder)
Variant Entry
Field Type Required Description namestring✅ Variant display name propsRecord<string, unknown>❌ Props for this variant, overrides component-level props
Modes
Auto Mode (recommended for new projects)
Zero config. Stargazer recursively scans src/ and discovers all
.astro files. Layouts, pages and styles are excluded by default.
Component and layout discovery happens at runtime — create a new
component, refresh the browser, and Stargazer picks it up instantly.
const config: StargazerConfig = {
mode: 'auto',
defaultLayout: 'main',
layouts: { main: './src/layouts/Layout.astro' },
};
Auto mode + multiple layouts (layoutMap)
If your project has more than one layout, use layoutMap to assign
layouts by directory. More specific paths win.
const config: StargazerConfig = {
mode: 'auto',
defaultLayout: 'main',
layouts: {
main: './src/layouts/Layout.astro',
blog: './src/layouts/BlogLayout.astro',
},
layoutMap: {
'src/components/blog': 'blog',
'src/components': 'main',
},
};
Auto mode + global defaults
Most projects have components that require common props like lang, theme, or a logged-in user object. Use defaults to pass them to every component automatically:
const config: StargazerConfig = {
mode: 'auto',
defaults: {
lang: 'en',
theme: 'default',
},
defaultLayout: 'main',
layouts: { main: './src/layouts/Layout.astro' },
};
Important: In auto mode, components that require
props but don't receive them will throw a runtime error in the preview.
Add any required props to defaults to fix this.
Files Mode
All components listed in stargazer.config.ts:
const config: StargazerConfig = {
mode: 'files',
components: [
{ name: 'Hero', path: './src/components/Hero.astro', description: 'Landing hero section' },
{ name: 'Footer', path: './src/components/Footer.astro' },
],
};
Best for: Full control over what's shown — choose exactly which components appear and in what order.
Recipes
Common configuration patterns for real projects.
Design system — buttons, badges, cards with all variants
If you're building a component library, list every variant explicitly so
you can visually QA them all in one session:
const config: StargazerConfig = {
mode: 'files',
defaultLayout: 'main',
layouts: { main: './src/layouts/Layout.astro' },
components: [
{
name: 'Button',
path: './src/components/ui/Button.astro',
variants: [
{ name: 'Primary', props: { variant: 'primary', size: 'md' } },
{ name: 'Secondary', props: { variant: 'secondary', size: 'md' } },
{ name: 'Ghost', props: { variant: 'ghost', size: 'md' } },
{ name: 'Danger', props: { variant: 'danger', size: 'md' } },
{ name: 'Disabled', props: { variant: 'primary', disabled: true } },
{ name: 'Small', props: { variant: 'primary', size: 'sm' } },
{ name: 'Large', props: { variant: 'primary', size: 'lg' } },
],
},
{
name: 'Badge',
path: './src/components/ui/Badge.astro',
variants: [
{ name: 'Green', props: { color: 'green', label: 'Active' } },
{ name: 'Red', props: { color: 'red', label: 'Error' } },
{ name: 'Yellow', props: { color: 'yellow', label: 'Warning' } },
{ name: 'Blue', props: { color: 'blue', label: 'Info' } },
],
},
],
};
Multilingual site — preview every component in every locale
Use defaults for the fallback language and variants to preview other locales:
const config: StargazerConfig = {
mode: 'auto',
defaults: { lang: 'en' },
defaultLayout: 'main',
layouts: { main: './src/layouts/Layout.astro' },
components: [
{
name: 'Hero',
path: './src/components/Hero.astro',
variants: [
{ name: 'English', props: { lang: 'en' } },
{ name: 'Portuguese', props: { lang: 'pt' } },
{ name: 'Spanish', props: { lang: 'es' } },
],
},
{
name: 'Pricing',
path: './src/components/Pricing.astro',
variants: [
{ name: 'USD', props: { currency: 'USD', lang: 'en' } },
{ name: 'EUR', props: { currency: 'EUR', lang: 'pt' } },
],
},
],
};
Large project — auto mode with layout mapping by section
Use layoutMap when different sections of your site use different
layouts:
const config: StargazerConfig = {
mode: 'auto',
defaults: { lang: 'en', user: { name: 'Demo User', role: 'admin' } },
defaultLayout: 'main',
layouts: {
main: './src/layouts/Layout.astro',
blog: './src/layouts/BlogLayout.astro',
dashboard: './src/layouts/DashboardLayout.astro',
},
layoutMap: {
'src/components/dashboard': 'dashboard',
'src/components/blog': 'blog',
'src/components': 'main',
},
exclude: ['src/components/internal', 'src/components/deprecated'],
};
More specific paths win in layoutMap — src/components/dashboard takes priority over src/components for any component inside
it.
Variants
Variants let you preview the same component with different props — ideal
for design system components like buttons, badges, and cards.
{
name: 'Button',
path: './src/components/Button.astro',
variants: [
{ name: 'Primary', props: { variant: 'primary', size: 'md' } },
{ name: 'Secondary', props: { variant: 'secondary', size: 'md' } },
{ name: 'Ghost', props: { variant: 'ghost', size: 'md' } },
{ name: 'Danger', props: { variant: 'danger', size: 'md' } },
],
}
Each variant appears as a separate card in the index: Button / Primary, Button / Ghost, etc.
Example — Badge colors
{
name: 'Badge',
path: './src/components/Badge.astro',
variants: [
{ name: 'Green', props: { color: 'green', label: 'Active' } },
{ name: 'Red', props: { color: 'red', label: 'Error' } },
{ name: 'Yellow', props: { color: 'yellow', label: 'Warning' } },
{ name: 'Blue', props: { color: 'blue', label: 'Info' } },
],
}
Props Inheritance
Props are merged in this order (later wins):
Global defaults → Component props → Variant props
const config: StargazerConfig = {
defaults: { lang: 'en', colorMode: 'dark' },
components: [
{
name: 'Hero',
path: './src/components/Hero.astro',
props: { colorMode: 'light' }, // overrides default
variants: [
{ name: 'English' }, // gets: { lang: 'en', colorMode: 'light' }
{ name: 'Portuguese', props: { lang: 'pt' } }, // gets: { lang: 'pt', colorMode: 'light' }
],
},
],
};
Prop Global Component Variant (PT) Final lang'en'— 'pt''pt' colorMode'dark''light'— 'light'
Layout Wrapping
Components often need their parent layout for correct rendering — global
CSS, navigation, fonts, etc.
const config: StargazerConfig = {
defaultLayout: 'main',
layouts: {
main: './src/layouts/Layout.astro',
blog: './src/layouts/BlogLayout.astro',
},
components: [
{ name: 'Hero', path: './src/components/Hero.astro' },
// ↑ uses 'main' layout (from defaultLayout)
{ name: 'Blog Card', path: './src/components/BlogCard.astro', layout: 'blog' },
// ↑ uses 'blog' layout (override)
{ name: 'Icon', path: './src/components/Icon.astro', layout: undefined },
// ↑ no layout — renders standalone
],
};
-
With layout: component renders inside the layout as
<slot /> child
-
layout: undefined: component renders standalone, no
wrapping
-
No
layout field: defaultLayout is used
The isStargazer Prop
Stargazer automatically injects { isStargazer: true } into the Layout's Astro.props. Use it to hide navbars and
footers from previews:
---
// src/layouts/Layout.astro
const { isStargazer } = Astro.props;
---
<html lang="en">
<body>
{!isStargazer && <Header />}
<slot />
{!isStargazer && <Footer />}
</body>
</html>
Override Editor & Settings
Each component card has a ✎ button (always visible, yellow) that
opens the override editor modal. Changes are stored in localStorage — they never modify your config files.
Per-component override
Click ✎ on any card to:
- Switch layout — choose from any layout defined in
layouts - Set props — enter a JSON object that merges on top of existing
props
Global Settings & Visibility
The ⚙ Settings button in the status bar applies overrides
to every component in the session. Per-component overrides take priority:
component override > global override > config
Config default vs. session override: The defaultLayout in your stargazer.config.ts is the permanent baseline — what
runs when nothing is overridden. The layout you pick in the ⚙ Settings modal
is a temporary session override stored only in localStorage. Cards show Default Layout only when no override is active
and the component is using its config default. If you select a layout in
Settings (even the same one), it counts as an override and shows the layout
filename instead.
Inside the Settings modal, you have access to Visibility Toggles:
- Hide Categories: Uncheck an entire category to collapse
it and grey out all its children.
- Hide Components: Disable specific items to declutter your
search index.
- Persistence: Toggles persist in your browser and do not
mutate your team's config files.
CSS & Global Styles
Source Works? Why Scoped <style> inside .astro ✅ Always Collected from Vite's module graph Global CSS imported in a layout ✅ With layout Parsed from the layout file Global CSS not in a layout ❌ Only exists if the layout imports it
The fix: Always set a defaultLayout that imports
your global stylesheet. Components without a layout show ⚠ no layout in the index.
How it works under the hood: When a component is requested
for preview, Stargazer calls ssrLoadModule on the component
file. This populates Vite's module graph with the component's scoped style
modules (?astro&type=style). Stargazer then traverses
the module graph to collect these URLs and injects them as <link> tags alongside the layout's global CSS imports.
Viewport & Zoom
Viewport Buttons
Button Width Use case FULL 100% Current browser width 2560px 2560px 4K / Ultra-wide 1440px 1440px Standard desktop 1024px 1024px Tablet landscape / Small laptop 780px 780px Tablet portrait 390px 390px Mobile
Viewport > screen width: Preview scales down visually
but media queries respond to the real selected viewport width.
Zoom
Zoom appears only when viewport is ≤ 1024px:
Button Scale 100% 1.0 75% 0.75 50% 0.5 25% 0.25 — full page at a glance
Dark Mode
The dark/light toggle is enabled by default. Configure to match your
project:
// Disable entirely
darkMode: false
// Tailwind (class)
darkMode: { method: 'class' }
// DaisyUI / Pico CSS
darkMode: { method: 'data-theme' }
// Custom attribute (default behavior)
darkMode: { method: 'attribute', attribute: 'color-scheme' }
// Custom values
darkMode: { method: 'data-theme', dark: 'night', light: 'day' }
Full options
Option Type Default Description method'attribute' | 'class' | 'data-theme''attribute'How dark mode is applied attributestring'color-scheme'Attribute name (attribute method only) targetstring'html'CSS selector for the target element darkstring'dark'Value/class for dark mode lightstring'light'Value/class for light mode
Search
The index shows a search input when more than 4 components are registered.
-
Type to filter by component name or category
-
Press
/ anywhere on the index page to focus the search input
- Press
Escape to clear and blur the input - Categories with no matching components are hidden automatically
Category View All
Every category on the index page has a View All link. Clicking
it opens a scrollable page showing every component in that category rendered
at full size.
- All components render inside one shared Layout
- Components are stacked vertically, no iframes
- A sticky label marks each component boundary
-
All nav bar controls (viewport, zoom, dark mode) apply to the entire
page
Keyboard Shortcuts
Available on any preview page:
Key Action FSwitch to FULL viewport DSwitch to DARK theme LSwitch to LIGHT theme 1Zoom 100% 2Zoom 75% 3Zoom 50% (only when viewport ≤ 1024px) 4Zoom 25% (only when viewport ≤ 1024px) RReload the component iframe CCopy Props to clipboard ←Navigate to previous component →Navigate to next component /Focus search input (index only) EscapeClear search (index only)
Shortcuts are disabled when focus is inside a text input. Zoom shortcuts
(1–4) are only active when viewport is ≤
1024px — matching when the zoom buttons are visible.
Copy Props
The COPY PROPS button in the preview nav bar copies the current
component's props as formatted JSON to your clipboard. Useful for sharing
exact prop states with teammates or debugging.
URL State
Viewport and zoom are persisted in the URL query string:
/demo/hero?vp=390&zoom=0.75
- Paste the URL to share the exact view with a teammate
- State is also saved to
localStorage as a fallback -
When opening a fresh preview, Stargazer restores your last
viewport/zoom automatically
Hot Reload
When you edit stargazer.config.ts, the browser reloads
automatically — no need to restart the dev server.
- Only fires on
change events (not on file deletion) - 300ms debounce to avoid double triggers on save
-
Logs to console:
[astro-stargazer] Config reloaded — X component(s)
Error Boundary
If a component fails to load or throws at runtime, Stargazer shows a
styled error card instead of a blank page.
- Server-side errors — caught by the SSR route, shows error
message + file path + Reload button
- Client-side runtime errors — caught by
window.onerror, replaces the body
Fix the error in your component and click Reload.
Architecture
Your Astro Project
├── astro.config.mjs ← adds stargazer() integration
├── stargazer.config.ts ← your component registry
└── src/
├── components/ ← your components
└── layouts/ ← your layouts
astro-stargazer (npm package)
├── src/
│ ├── integration.ts ← Astro hooks, route injection, JSON API middleware
│ ├── scanner.ts ← component discovery (all three modes)
│ ├── generator.ts ← builds registry JSON
│ ├── types.ts ← TypeScript interfaces
│ ├── routes/
│ │ ├── index.astro ← component registry page with search
│ │ ├── preview.astro ← single component view shell
│ │ ├── category.astro ← category "View All" shell
│ │ ├── docs.astro ← this documentation page
│ │ ├── frame/[slug].astro ← renders a single component
│ │ ├── frame-cat/[...cat].astro ← renders all components in a category
│ │ └── _Nav.astro ← shared nav bar component
│ └── client/
│ └── controls.js ← viewport/zoom/theme/shortcuts
└── bin/
└── create-stargazer.mjs ← npx setup wizard
API Endpoints (Vite middleware)
Endpoint Description /__stargazer/registry.jsonFull component list + config /__stargazer/preview/:slugComponent metadata as JSON /__stargazer/paths/:slugReturns /@fs/ URLs for component + layout + CSS /__stargazer/render-category-meta/:catReturns JSON list of slugs for a category
Data flow
Single component view (/demo/[slug]):
-
Integration reads
stargazer.config.ts at dev server startup
-
Scanner resolves components and merges props (defaults → component →
variant)
-
Route
/demo/[slug] serves preview.astro —
an HTML shell with the shared nav bar
-
The shell renders an
<iframe> pointing to /demo/frame/[slug] -
The frame route dynamically imports the Layout and Component, and
renders them as a native Astro page
-
CSS is collected from two sources: Layout file CSS imports and
component scoped styles (via
ssrLoadModule + module graph traversal)
Platform Compatibility
Astro Stargazer runs on macOS, Linux and Windows — any OS
that supports Node.js ≥ 18.
- Pure Node.js — no native binaries
- No shell scripts or OS-specific paths
-
The wizard uses only
readline and fs from the
Node standard library
Framework Compatibility
Stargazer previews .astro files. Since Astro natively supports React, Vue, Svelte, Solid, Preact, Alpine and others via its island architecture, any framework component imported
inside an .astro file works automatically.
---
// src/components/Card.astro
import Button from './Button.tsx'; // React
import Tooltip from './Tooltip.vue'; // Vue
import Badge from './Badge.svelte'; // Svelte
---
<Button variant="primary" />
<Tooltip text="hello" client:hover />
<Badge color="green" />
Stargazer previews Card.astro and everything inside it renders
exactly as it would in your real page — hydration directives included.
Contributing
Want to help? Contributions are welcome. Whether it's bug reports,
feature ideas, or PRs for other framework support, feel free to jump in.
Check the issues or just open one.
Roadmap
React, Vue, Svelte, Solid components work today — if you use them inside .astro files, Stargazer previews them
with full hydration support.
The longer-term plan is standalone packages for projects that don't use Astro at all:
-
next-stargazer — for pure Next.js / React projects
-
svelte-stargazer — for pure SvelteKit projects
-
nuxt-stargazer — for pure Nuxt / Vue projects
No timeline on these yet. Astro is the focus until v1.0 is solid.
Troubleshooting
Components not showing up
-
Check that
stargazer.config.ts exists in project root
-
Verify
path values start with ./ and are relative
to project root
-
Run
npm run dev and check console for [astro-stargazer] Found X component(s) -
In
mode: 'auto', make sure the component is inside src/ and not in an excluded path
Styles missing in preview
-
Set
defaultLayout to a layout that imports your global CSS
-
Components without a layout show
⚠ no layout in the index
Layout not wrapping correctly
-
Ensure your layout uses
<slot /> to render children
-
Verify the layout key in
components matches a key in layouts
Stargazer not in production
By design — dev-only. Set buildable: true to include it in production
builds.
Important: When using buildable: true,
your Astro project must have a server adapter configured (Cloudflare, Vercel, Netlify, Node, etc.). Stargazer's routes are SSR-only.
Dark mode toggle not working
CSS must respond to the method you configured:
-
attribute: [color-scheme="dark"] on <html> class: html.dark -
data-theme: [data-theme="dark"] on <html>
Search bar not showing up
The search input only appears when there are more than 4 components registered.
Copy Props button doesn't work
The Clipboard API requires either HTTPS or localhost.
It won't work over plain HTTP on a network address.
Zoom buttons are not showing
Zoom only appears when the selected viewport is ≤ 1024px. At larger viewports there is no zoom.
Can I change the /demo URL?
Yes. Set the base option in your config:
const config: StargazerConfig = {
base: '/preview',
};
My site switched to hybrid output — will this break my build?
No. Stargazer temporarily switches output to hybrid in memory during astro dev so its SSR routes
work. This change is never written to your config file and does not affect
astro build.
Command Effect astro devStargazer active, output set to hybrid in memory only astro buildStargazer skipped entirely — your output stays static astro build + buildable: trueStargazer included, output set to hybrid, requires a server adapter
What adapter should I use with buildable: true?
Any official Astro adapter works: @astrojs/cloudflare, @astrojs/vercel, @astrojs/netlify, @astrojs/node. Configure
it as you normally would — Stargazer does not add any adapter-specific
code.
If your project currently uses output: 'static' with no adapter,
the simplest option for staging is @astrojs/node in standalone
mode. Or just leave buildable at its default false and use Stargazer in dev only.
Component shows an error page
The error boundary caught a failure. Read the error message shown in the
iframe, fix the component, click Reload.
If the error says Cannot read properties of undefined, the component is trying to use a prop that wasn't passed. This is
common in auto mode. Fix by adding the required prop to defaults:
const config: StargazerConfig = {
mode: 'auto',
defaults: {
lang: 'en', // fix: 'Cannot read properties of undefined (reading "contact")'
},
};
Preview is blank but shows no error
The component probably renders no visible HTML — empty output,
whitespace only, or a display: none element. Check the component
itself and open the browser devtools inside the iframe to inspect.