Complete reference for all 20+ SpaceZero UI components with props, variants, and usage examples.
Complete reference for all SpaceZero UI components.
src/index.cssimport { Button } from '@/components/ui/button';
<Button variant="action" href="/path">Link Button</Button>
<Button variant="default" onClick={handler}>Default</Button>
<Button variant="destructive" disabled>Deleting...</Button>
Props:
| Prop | Type | Default | Description |
|---|---|---|---|
| variant | 'default', 'action', 'success', 'warning', 'destructive' | 'default' | Visual style |
| href | string | - | Renders as <a> |
| disabled | boolean | false | Disables interaction |
| className | string | - | Extra classes |
There is no loading prop — use disabled plus your own label while a request is in flight.
import { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter } from '@/components/ui/card';
<Card>
<CardHeader>
<CardTitle><Icon name="Server" /><span>Feature</span></CardTitle>
<Badge>New</Badge>
</CardHeader>
<CardDescription>Description text</CardDescription>
<CardContent>Main content</CardContent>
<CardFooter>Footer actions</CardFooter>
</Card>
Sub-components:
Card - Container with border/bgCardHeader - Header sectionCardTitle - Title with icon supportCardDescription - Muted descriptionCardContent - Body contentCardFooter - Footer areaimport { Hero, HeroTitle, HeroDescription } from '@/components/ui/hero';
<Hero>
<Badge>Tag</Badge>
<HeroTitle>Hero Title</HeroTitle>
<HeroDescription>Description text</HeroDescription>
</Hero>
Guidance: HeroTitle uses text-5xl with font-hero for all headings.
import { Input } from '@/components/ui/input';
<Input
type="email"
placeholder="Enter email"
value={email}
onChange={e => setEmail(e.target.value)}
togglePassword
required
/>
Props:
| Prop | Type | Description |
|---|---|---|
| togglePassword | boolean | Shows eye icon for password (default true) |
| All standard input props |
Padding: py-2 px-3 (0.5rem / 0.75rem).
import { Textarea } from '@/components/ui/textarea';
<Textarea
placeholder="Enter text..."
value={text}
onChange={e => setText(e.target.value)}
rows={4}
/>
Touch Target: py-3 padding with min-h-16 (64px minimum height)
import { Select } from '@/components/ui/select';
<Select
options={[
{ label: 'Option 1', value: '1' },
{ label: 'Option 2', value: '2', disabled: true },
]}
value={selected}
onValueChange={setSelected}
placeholder="Select..."
searchable
multiple
clearable
/>
Props:
| Prop | Type | Description |
|---|---|---|
| options | SelectOption[] | Required options array |
| value / defaultValue | string | Single select |
| values / defaultValues | string[] | Multi select |
| onValueChange | (v: string) => void | Single callback |
| onValuesChange | (v: string[]) => void | Multi callback |
| searchable | boolean | Enable search filter |
| multiple | boolean | Enable multi-select with chips |
| clearable | boolean | Show clear all button |
| placeholder | string | Single placeholder |
| multiplePlaceholder | string | Multi placeholder |
| disabled | boolean | Disable interaction |
Touch Targets: Trigger uses py-2 px-3; options use py-2
import { Modal, ModalBackdrop, ModalDialog, ModalClose, ModalHeader, ModalIcon, ModalTitle, ModalDescription, ModalFooter, ModalBody } from '@/components/ui/modal';
import { Notice } from '@/components/ui/notice';
<Modal>
<ModalBackdrop />
<ModalDialog>
<ModalClose onClick={close}>
<Icon name="X" />
</ModalClose>
<ModalHeader>
<ModalIcon><Icon name="CheckCircle2" /></ModalIcon>
<ModalTitle>Success</ModalTitle>
</ModalHeader>
<ModalDescription>Operation completed.</ModalDescription>
<ModalBody>
<Notice>Additional info</Notice>
</ModalBody>
<ModalFooter>...</ModalFooter>
</ModalDialog>
</Modal>
Modal is a plain overlay — visibility is controlled by conditionally rendering it (no isOpen/onClose props).
Sub-components:
Modal - Fixed overlay containerModalBackdrop - Blurred + tinted backdropModalDialog - Dialog box (w-full max-w-full rounded-5 p-5 md:max-w-lg)ModalClose - Close button (p-2)ModalHeader - Centered headerModalIcon - Icon containerModalTitle - TitleModalDescription - DescriptionModalBody - Scrollable bodyModalFooter - Footer actionsNotice - Alert box (variant: warning/destructive)import { Accordion, AccordionItem } from '@/components/ui/accordion';
<Accordion defaultValue="item-1">
<AccordionItem value="item-1" title="What is this?">
<p>Explanation here</p>
</AccordionItem>
<AccordionItem value="item-2" title="How does it work?">
<p>Details here</p>
</AccordionItem>
</Accordion>
import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs';
<Tabs defaultValue="overview">
<TabsList>
<TabsTrigger value="overview">Overview</TabsTrigger>
<TabsTrigger value="settings">Settings</TabsTrigger>
</TabsList>
<TabsContent value="overview"><p>Content</p></TabsContent>
<TabsContent value="settings"><p>Content</p></TabsContent>
</Tabs>
import { Badge } from '@/components/ui/badge';
<Badge variant="default">Default</Badge>
<Badge variant="action">Action</Badge>
<Badge variant="success">Success</Badge>
<Badge variant="warning">Warning</Badge>
<Badge variant="destructive">Destructive</Badge>
import { Progress } from '@/components/ui/progress';
<Progress value={75} max={100} label="Upload Progress" />
import { Spinner } from '@/components/ui/spinner';
<Spinner size={24} />
Only allowed animation in SpaceZero.
import { ToastProvider, useToast } from '@/components/ui/toast';
// Wrap app
<ToastProvider><App /></ToastProvider>
// In component
const { toast } = useToast();
toast.success('Saved!');
toast.destructive('Failed', 'Try again');
toast.warning('Warning');
toast.action('Info');
import { Icon } from '@/components/ui/icon';
<Icon name="Server" size={20} color="var(--action)" />
56 icons available (Lucide + a few brand icons from react-icons). Change DEFAULT_ICON_SIZE to resize all globally.
import { Link } from '@/components/ui/link';
<Link href="/path" variant="default">Link</Link>
<Link href="/path" variant="small">Small</Link>
<Link href="/path" variant="muted">Muted</Link>
<Link href="/path" variant="header">Header</Link>
<Link href="mailto:email" variant="email">Email</Link>
import { Code, CodeBlockTitle, CodeBlockItem, CodeInline } from '@/components/ui/code';
<Code>
<CodeBlockTitle><Icon name="Terminal" /><span>Setup</span></CodeBlockTitle>
<CodeBlockItem>Step 1</CodeBlockItem>
<CodeBlockItem>Step 2</CodeBlockItem>
</Code>
<CodeInline>npm install</CodeInline>
import { Article, ArticleSection, ArticleHeading, ArticleParagraph, ArticleList, ArticleListItem, ArticleStrong } from '@/components/ui/article';
<Article>
<ArticleSection>
<ArticleHeading>Section</ArticleHeading>
<ArticleParagraph>Text with <ArticleStrong>bold</ArticleStrong>.</ArticleParagraph>
<ArticleList>
<ArticleListItem>Item 1</ArticleListItem>
<ArticleListItem>Item 2</ArticleListItem>
</ArticleList>
</ArticleSection>
</Article>
import { NewsletterForm } from '@/components/ui/newsletter';
<NewsletterForm
title="Stay Updated"
description="Get the latest news"
buttonText="Subscribe"
/>
import { SkeletonLoader, SkeletonPost } from '@/components/infra/skeleton-loader';
<SkeletonLoader className="h-5 w-full" />
<SkeletonPost />
import { Toggle } from '@/components/ui/toggle';
<Toggle
checked={enabled}
onChange={setEnabled}
label="Enable feature"
variant="action"
/>
Props:
| Prop | Type | Description |
|---|---|---|
| checked | boolean | Controlled state |
| onChange | (checked: boolean) => void | Change handler |
| disabled | boolean | Disable interaction |
| label | string | Accessible label |
| variant | 'default', 'action', 'success', 'warning', 'destructive' | Track color |
Uses role="switch".
import { PricingCard } from '@/components/ui/pricing';
<PricingCard
gumroadProductId="myapp"
priceDisplay="
originalPriceDisplay="
planName="Lifetime Access"
ctaText="Get Lifetime Access"
/>
All props default to appConfig.monetization. The CTA links to https://{gumroadProductId}.gumroad.com/l/{gumroadProductId}.
import { Auth } from '@/components/ui/auth';
<Auth isOpen={open} onClose={close} />
Props:
| Prop | Type | Description |
|---|---|---|
| isOpen | boolean | Controls visibility |
| onClose | () => void | Close handler |
| title | string | Modal title |
| description | string | Modal description |
Renders OAuth (Google/GitHub) + email/password forms with password strength validation, backed by useAuth().
import { PageLayout, PageMain } from '@/components/layout/wireframe';
<PageLayout>
<PageMain>Content</PageMain>
</PageLayout>
import { Section } from '@/components/layout/section';
<Section cols={2}> {/* 1 or 2 */}
<Card>...</Card>
<Card>...</Card>
</Section>
import { ProtectedRoute } from '@/components/infra/protected-route';
<ProtectedRoute requireAuth redirect="/">
<PrivatePage />
</ProtectedRoute>
import { ErrorBoundary } from '@/components/infra/error-boundary';
<ErrorBoundary>
<App />
</ErrorBoundary>
const {
user, session, loading,
signInWithOAuth, signInWithEmail, signUpWithEmail,
resetPassword, sendConfirmationEmail, checkUserExists,
updatePassword, signOut,
} = useAuth();
const { toast } = useToast();
toast.success('Message');
toast.destructive('Message', 'Description');
| Class | CSS Variable | Value |
|---|---|---|
| 0 | --scaling-0 | 0 |
| 1 | --scaling-1 | 0.25rem |
| 2 | --scaling-2 | 0.5rem |
| 3 | --scaling-3 | 0.75rem |
| 5 | --scaling-5 | 1.25rem |
| 9 | --scaling-9 | 2.25rem |
| 12 | --scaling-12 | 3rem |
| 16 | --scaling-16 | 5rem |
| 48 | --scaling-48 | 12rem |
| 96 | --scaling-96 | 24rem |
| lg | --scaling-lg | 32rem |
| 7xl | --scaling-7xl | 5rem |
| full | - | 100% |
| auto | - | auto |
| px | - | 1px |
Applies to every spacing/sizing utility: p-*, m-*, gap-*, w-*, h-*, insets.
| Class | Tailwind Default | Value | Use |
|---|---|---|---|
| text-5xl | 3rem | 3rem | Hero titles |
| text-lg | 1.125rem | 1.125rem | Headings |
| text-base | 1rem | 1rem | Body text |
| text-xs | 0.75rem | 0.75rem | Captions |
| Class | CSS Variable | Value |
|---|---|---|
| rounded-0 | --radius-0 | 0 |
| rounded-5 | --radius-5 | 0.5rem |
| rounded-full | --radius-full | 9999px |
| Category | Tokens |
|---|---|
| Text | regular, accent, warning, success, muted |
| Background | surface, layer, action, codeblock |
| Border | border |
| Input | input |
| Destructive | destructive |
Only font-semibold (600) allowed.
CardTitle not <h3 className="...">sm:, md:, lg:, xl:, 2xl: are permitted where a fluid layout cannot workpy-2/py-3 scale tokenstext-regular, bg-layer, border-bordertext-xs, text-base, text-lg, text-5xl<a>href<input>